fix meta date
[ikiwiki/events.git] / events.pm
1 #! /usr/bin/perl
2 # Copyright (C) 2014 Julien Moutinho <julm+ikiwiki+events&autogeree.net>
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License,
7 # or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 package IkiWiki::Plugin::events;
19
20 use strict;
21 use warnings;
22 use IkiWiki 3.00;
23 use Time::Local;
24 use DateTime;
25 use CGI::FormBuilder;
26 #use Data::Dumper;
27
28 sub import {
29 hook(type => "getsetup", id => "events", call => \&getsetup);
30 hook(type => "needsbuild", id => "events", call => \&needsbuild);
31 hook(type => "preprocess", id => "events", call => \&preprocess);
32 hook(type => "sessioncgi", id => "events", call => \&sessioncgi);
33 }
34 sub getsetup () {
35 return
36 { plugin =>
37 { safe => 1
38 , rebuild => undef
39 , section => "widget"
40 }
41 };
42 }
43
44 my $now
45 = DateTime->now
46 ( time_zone => 'local'
47 , locale => $config{locale}
48 )->set_time_zone('floating');
49 my @days = ('01'..'31');
50 my @hours = ('00'..'23');
51 my @minutes = ('00'..'59');
52
53 # update
54 sub set_rendering_expiration ($$) {
55 my ($page, $timestamp) = @_;
56 if (not exists $pagestate{$page}{events}{expiration}
57 or $timestamp < $pagestate{$page}{events}{expiration}) {
58 my $time = DateTime->from_epoch
59 ( epoch => $timestamp
60 , time_zone => 'UTC'
61 , locale => $config{locale}
62 );
63 #debug("events: set_rendering_expiration(): will refresh: page=".$page
64 # . " after: date=".$time->strftime('%Y-%m-%d_%H-%M-%S'));
65 $pagestate{$page}{events}{expiration} = $timestamp;
66 }
67 }
68 sub set_next_rendering (%) {
69 my %params = @_;
70 if ($params{type} eq 'month'
71 and $params{focus}->year() == $now->year()
72 and $params{focus}->month() == $now->month()) {
73 # NOTE: calendar for current month, updates next day
74 my $update = $params{focus}->clone;
75 $update->set_hour(0);
76 $update->set_minute(0);
77 $update->set_second(0);
78 $update->set_nanosecond(0);
79 my $duration = DateTime::Duration->new(days => 1, end_of_month => 'limit');
80 $update->add_duration($duration);
81 set_rendering_expiration($params{destpage}, $update->epoch());
82 #debug("events: will refresh current month: page=".$params{destpage}
83 # . " after: date=".$update->strftime('%Y-%m-%d_%H-%M-%S'));
84 }
85 elsif ($params{type} eq 'month'
86 and (( $params{focus}->year() == $now->year()
87 and $params{focus}->month() > $now->month())
88 or $params{focus}->year() > $now->year())) {
89 # NOTE: calendar for upcoming month, updates 1st of next month
90 my $update = $params{focus}->clone;
91 $update->set_day(1);
92 $update->set_hour(0);
93 $update->set_minute(0);
94 $update->set_second(0);
95 $update->set_nanosecond(0);
96 set_rendering_expiration($params{destpage}, $update->epoch());
97 #debug("events: will refresh upcoming month: page=".$params{destpage}
98 # . " after: date=".$update->strftime('%Y-%m-%d_%H-%M-%S'));
99 }
100 elsif ($params{type} eq 'day'
101 and ($params{focus}->year() == $now->year()
102 and $params{focus}->month() == $now->month()
103 and $params{focus}->day() == $now->day())) {
104 # NOTE: calendar for current day, updates next day
105 my $update = $params{focus}->clone;
106 $update->set_hour(0);
107 $update->set_minute(0);
108 $update->set_second(0);
109 $update->set_nanosecond(0);
110 my $duration = DateTime::Duration->new(days => 1, end_of_month => 'limit');
111 $update->add_duration($duration);
112 set_rendering_expiration($params{destpage}, $update->epoch());
113 #debug("events: will refresh current day: page=".$params{destpage}
114 # . " after: date=".$update->strftime('%Y-%m-%d_%H-%M-%S'));
115 }
116 elsif ($params{type} eq 'day'
117 and (( $params{focus}->year() == $now->year()
118 and ( $params{focus}->month() > $now->month()
119 or ($params{focus}->month() == $now->month()
120 and $params{focus}->day() > $now->day() ))
121 or $params{focus}->year() > $now->year()))) {
122 # NOTE: calendar for upcoming day, updates that day
123 my $update = $params{focus}->clone;
124 $update->set_hour(0);
125 $update->set_minute(0);
126 $update->set_second(0);
127 $update->set_nanosecond(0);
128 set_rendering_expiration($params{destpage}, $update->epoch());
129 #debug("events: will refresh upcoming day: page=".$params{destpage}
130 # . " after: date=".$update->strftime('%Y-%m-%d_%H-%M-%S'));
131 }
132 }
133 sub needsbuild (@) {
134 my $needsbuild = shift;
135 foreach my $page (keys %pagestate) {
136 if (exists $pagestate{$page}{events}{expiration}) {
137 if ($pagestate{$page}{events}{expiration} <= $now->epoch()) {
138 # NOTE: force a rebuild so the calendar shows the current day
139 push @$needsbuild, $pagesources{$page};
140 }
141 if (exists $pagesources{$page}
142 and grep { $_ eq $pagesources{$page} } @$needsbuild) {
143 # NOTE: remove state, will be re-added
144 # if the calendar is still there during the rebuild
145 delete $pagestate{$page}{events};
146 }
147 }
148 }
149 return $needsbuild;
150 }
151
152 # render
153 sub date_of_page ($%) {
154 my ($page, %params) = @_;
155 my $dir = IkiWiki::dirname($page);
156 my ($year, $month, $day, $hour, $hour_begin, $hour_end)
157 = $dir =~ m{
158 .*/
159 (\d+)/
160 (01|02|03|04|05|06|07|08|09|10|11|12)/
161 (01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31)/
162 (([0-2][0-9]h[0-5][0-9])(-[0-2][0-9]h[0-5][0-9])?)?
163 $
164 }x;
165 my $r =
166 { year => $year
167 , month => $month
168 , day => $day
169 , hour => $hour
170 , hour_begin => $hour_begin
171 , hour_end => $hour_end
172 };
173 #debug("date_of_page: dir=".$dir." ".Dumper($r));
174 return $r;
175 }
176 sub event_of_page ($%) {
177 my ($event, %params) = @_;
178 my $title
179 = exists $pagestate{$event}{meta}{title}
180 ? $pagestate{$event}{meta}{title}
181 : pagetitle(IkiWiki::basename($event));
182 my $hour
183 = date_of_page($event)->{hour};
184 my $link
185 = htmllink
186 ( $params{page}
187 , $params{destpage}
188 , $event
189 , linktext => $title
190 , noimageinline => 1
191 , title => $title );
192 my @tags
193 = sort {lc $a cmp lc $b}
194 (grep {
195 not defined $params{tags}
196 or pagespec_match($_, $params{tags})
197 }
198 (keys %{$IkiWiki::typedlinks{$event}{tag}}));
199 @tags
200 = map {
201 my $tag = $_;
202 my $title
203 = exists $pagestate{$tag}{meta}{title}
204 ? $pagestate{$tag}{meta}{title}
205 : pagetitle(IkiWiki::basename($tag));
206 my $link
207 = htmllink
208 ( $params{page}
209 , $params{destpage}
210 , '/'.$tag
211 , linktext => $title
212 , noimageinline => 1
213 , title => $title );
214 add_depends($params{page}, $event, deptype('content'));
215 #add_depends($params{page}, $tag, deptype('content'));
216 # XXX: much too heavy :\ and midnight refresh may fix it anyway.
217 my $class = qq{$tag};
218 $class =~ s{[^a-zA-Z0-9-]}{_}g;
219 { class => "tag tag-$class"
220 , link => $link }
221 } @tags;
222 my $base = IkiWiki::dirname($event);
223 $base =~ s/[^a-zA-Z0-9-]/_/g;
224 return
225 { hour => $hour
226 , link => $link
227 , tags => \@tags
228 , base => $base };
229 }
230 sub events_of_pages ($%) {
231 my ($pages, %params) = @_;
232 my @day_events = ();
233 my @hour_events = ();
234 my $pagedir = sub { IkiWiki::basename(IkiWiki::dirname(shift)) };
235 foreach my $page (@$pages) {
236 my $date = date_of_page($page);
237 if (defined $date->{hour}) {
238 push @hour_events, $page;
239 }
240 else {
241 push @day_events, $page;
242 }
243 }
244 return
245 map {event_of_page($_, %params)}
246 ( (sort {lc $pagedir->($a) cmp lc $pagedir->($b)} @day_events)
247 , (sort {lc $pagedir->($a) cmp lc $pagedir->($b)} @hour_events) );
248 }
249 sub event_html ($$%) {
250 my ($date, $format, %params) = @_;
251 my $day = sprintf("%02d", $date->day());
252 my $month = sprintf("%02d", $date->month());
253 my $year_html = $date->year();
254 my $month_html
255 = $format->{month_name}
256 ? $date->month_name()
257 : $month;
258 my $day_html
259 = $format->{day_name}
260 ? $date->day_name()." ".$date->day()
261 : $format->{day_abbr}
262 ? $date->day_abbr()." ".$date->day()
263 : $date->day();
264 my $wday_class
265 = "wday-".($date->day_of_week() - 1)
266 . ( ($date->year() == $now->year()
267 and $date->month() == $now->month()
268 and $date->day() == $now->day())
269 ? " today"
270 : "" );
271 my $new_html = "";
272 if (not defined $params{new} or $params{new} ne 'no') {
273 if ($format->{year}) {
274 my $year_page
275 = sprintf
276 ( '%s/%d'
277 , $params{base}
278 , $date->year()
279 );
280 add_depends($params{page}, $year_page, deptype("presence"));
281 if ($pagesources{$year_page}) {
282 $year_html
283 = htmllink
284 ( $params{page}
285 , $params{destpage}
286 , $year_page
287 , linktext => $year_html
288 , noimageinline => 1 );
289 }
290 }
291 if ($format->{month}) {
292 my $month_page
293 = sprintf
294 ( '%s/%d/%02d'
295 , $params{base}
296 , $date->year()
297 , $date->month()
298 );
299 add_depends($params{page}, $month_page, deptype("presence"));
300 if ($pagesources{$month_page}) {
301 $month_html
302 = htmllink
303 ( $params{page}
304 , $params{destpage}
305 , $month_page
306 , linktext => $month_html
307 , noimageinline => 1 );
308 }
309 }
310 if ($format->{day}) {
311 my $day_page
312 = sprintf
313 ( '%s/%d/%02d/%02d'
314 , $params{base}
315 , $date->year()
316 , $date->month()
317 , $date->day()
318 );
319 add_depends($params{page}, $day_page, deptype("presence"));
320 if ($pagesources{$day_page}) {
321 $day_html
322 = htmllink
323 ( $params{page}
324 , $params{destpage}
325 , $day_page
326 , linktext => $day_html
327 , noimageinline => 1 );
328 }
329 }
330 unless ($params{nonew} or not $format->{new}) {
331 $new_html
332 .= qq{<a class='new' href='}
333 . IkiWiki::cgiurl
334 ( base => $params{base}
335 , ($date->day()
336 ?(day => $day):())
337 , (($date->month() or $date->day())
338 ?(month => $month):())
339 , (($date->year() or $date->month() or $date->day())
340 ?(year => $date->year()):())
341 , do => 'newevent'
342 , page => $params{destpage}
343 )
344 . qq{' rel='nofollow'>+</a>};
345 }
346 }
347 return
348 { new => $new_html
349 , day => $day_html
350 , month => $month_html
351 , year => $year_html
352 , wday => $wday_class
353 };
354 }
355 sub preprocess_day (@) {
356 my %params = @_;
357 my @pages
358 = pagespec_match_list
359 ( $params{page}
360 , $params{pages}
361 , deptype => deptype("presence")
362 # NOTE: add presence dependencies to update calendar when pages are added/removed
363 );
364 my $event_html
365 = event_html
366 ( $params{focus}
367 , {day=>1, day_name=>1, new => 1}
368 , %params );
369 my @events
370 = map {
371 my @tags
372 = map {"<li\n class='".$_->{class}."'>".$_->{link}."</li>"}
373 @{$_->{tags}};
374 "<ul\n class='events'><li class='event event-$_->{base}'>"
375 . "<span class='head'>"
376 . (defined $_->{hour} ? "<span class='hour'>$_->{hour}</span>" : "")
377 . "<span class='link'>$_->{link}</span>"
378 . "</span>"
379 . "<ul\n class='tags'>".join("", @tags)."</ul>"
380 . "</li></ul>"
381 }
382 events_of_pages(\@pages, %params);
383 return
384 "<table\n class='wday'>"
385 . "<thead><tr><th><span\n class='head'><span\n class='day'>"
386 . $event_html->{day}
387 . $event_html->{new}
388 . "</span></span></th></tr></thead>"
389 . "<tbody><tr><td\n class='wday $event_html->{wday}'>"
390 . join("", @events)
391 . "</td></tr></tbody>"
392 . "</table>";
393 }
394 sub preprocess_month (@) {
395 my %params = @_;
396 my $one_day = DateTime::Duration->new(days => 1, end_of_month => 'limit');
397 my $day = $params{focus}->clone->set_day(1);
398 my $last_day
399 = DateTime->last_day_of_month
400 ( year => $params{focus}->year()
401 , month => $params{focus}->month() )->day();
402
403 my @pages
404 = pagespec_match_list
405 ( $params{page}
406 , $params{pages}
407 , deptype => deptype("presence")
408 # NOTE: add presence dependencies to update calendar when pages are added/removed
409 );
410
411 # NOTE: sort the pages by days of the month
412 my %days = map {($_=>[])} (1 .. $last_day);
413 foreach my $page (@pages) {
414 my $page_ctime = DateTime->from_epoch
415 ( epoch => $IkiWiki::pagectime{$page}
416 , time_zone => 'local'
417 , locale => $config{locale}
418 );
419 push @{$days{$page_ctime->day()}}, $page;
420 }
421
422 my $t='<tr>';
423 my $first_wday = $day->clone();
424 my $last_wday = ($params{week_start_day} + 6) % 7;
425 while ($first_wday->day_of_week() - 1 != $params{week_start_day}) {
426 # NOTE: pad the begining
427 $first_wday->subtract_duration($one_day);
428 $t.="<td class='no-wday'></td>";
429 }
430 my $month = $day->month();
431 for (; $day->month() == $month; $day->add_duration($one_day)) {
432 my $event_html
433 = event_html
434 ( $day
435 , {day=>1, day_abbr=>1, new => 1}
436 , %params );
437 $t.= "<td class='wday $event_html->{wday}'>";
438 my @events
439 = map {
440 my @tags
441 = map {"<li\n class='".$_->{class}."'>".$_->{link}."</li>"}
442 @{$_->{tags}};
443 "<li\n class='event event-$_->{base}'>"
444 . "<span class='head'>"
445 . (defined $_->{hour} ? "<span class='hour'>$_->{hour}</span>" : "")
446 . "<span class='link'>$_->{link}</span>"
447 . "</span>"
448 . "<ul class='tags'>".join("", @tags)."</ul>"
449 . "</li>\n"
450 }
451 events_of_pages($days{$day->day()}, %params);
452 $t .=
453 "<span class='head'>"
454 . "<span class='day'>"
455 . $event_html->{day}
456 . $event_html->{new}
457 . "</span>"
458 . "</span>"
459 . "<ul class='events'>".join("", @events)."</ul>";
460 $t.='</td>';
461 if ($day->day_of_week() - 1 == $last_wday) {
462 $t.="</tr>";
463 $t.="<tr>"
464 if ($day->day_of_month() < $last_day);
465 }
466 }
467 while ($day->day_of_week() - 1 != $params{week_start_day}) {
468 # NOTE: pad the end
469 $day->add_duration($one_day);
470 $t.="<td class='no-wday'></td>";
471 }
472 $t.='</tr>';
473 my $event_html
474 = event_html
475 ( $params{focus}
476 , {year=>1, month=>1, month_name=>1}
477 , %params );
478 return
479 "<table class='month'>"
480 . "<thead>"
481 . "<tr>"
482 . "<th colspan='7'>"
483 . "<span class='month'>$event_html->{month}</span>"
484 . " <span class='year'>$event_html->{year}</span>"
485 . "</th>"
486 . "</tr>"
487 . "<tr>"
488 . join ("", map {
489 $_ = "<th><span>".$first_wday->day_name()."</span></th>";
490 $first_wday->add_duration($one_day);
491 $_ } (1..7))
492 . "</tr>"
493 . "</thead>"
494 . "<tbody>$t</tbody></table>";
495 }
496 sub preprocess (@) {
497 my %params = @_;
498 $params{focus} = $now->clone;
499
500 $params{pages} = "*" unless defined $params{pages};
501 $params{type} = "month" unless defined $params{type};
502 $params{week_start_day} = 0 unless defined $params{week_start_day};
503 $params{week_start_day} = $params{week_start_day} % 7;
504
505 unless (defined $params{base}) {
506 $params{base}
507 = defined $config{events_base}
508 ? $config{events_base}
509 : gettext('Agenda');
510 }
511 if (defined $params{day}) {
512 if ($params{day} =~ m/^([+-])(\d+)$/) {
513 my ($sign, $days) = ($1, $2);
514 my $duration = DateTime::Duration->new(days => $days, end_of_month => 'limit');
515 $params{focus}
516 = $sign eq '+'
517 ? $params{focus}->add_duration($duration)
518 : $params{focus}->subtract_duration($duration);
519 }
520 else {
521 $params{focus}->set(day => $params{day});
522 }
523 }
524 else {
525 #$params{focus}->set(day => 1);
526 }
527 if (defined $params{month}) {
528 if ($params{month} =~ m/^([+-])(\d+)$/) {
529 my ($sign, $months) = ($1, $2);
530 my $duration = DateTime::Duration->new(months => $months, end_of_month => 'limit');
531 $params{focus}
532 = $sign eq '+'
533 ? $params{focus}->add_duration($duration)
534 : $params{focus}->subtract_duration($duration);
535 }
536 else {
537 $params{focus}->set(month => $params{month});
538 }
539 }
540 if (defined $params{year}) {
541 if ($params{year} =~ m/^([+-])(\d+)$/) {
542 my ($sign, $years) = ($1, $2);
543 my $duration = DateTime::Duration->new(years => $years, end_of_month => 'limit');
544 $params{focus}
545 = $sign eq '+'
546 ? $params{focus}->add_duration($duration)
547 : $params{focus}->subtract_duration($duration);
548 }
549 else {
550 $params{focus}->set(year => $params{year});
551 }
552 }
553
554 #debug("events: focus=".$params{focus}->strftime('%Y-%m-%d_%H-%M-%S'));
555 $params{pages} =~ s[%Y][$params{focus}->year()]eg;
556 $params{pages} =~ s[%m][sprintf('%02d', $params{focus}->month())]eg;
557 $params{pages} =~ s[%d][sprintf('%02d', $params{focus}->day())]eg;
558
559 set_next_rendering(%params);
560
561 my $calendar = "";
562 if ($params{type} eq 'month') {
563 $calendar = preprocess_month(%params);
564 }
565 elsif ($params{type} eq 'day') {
566 $calendar = preprocess_day(%params);
567 }
568
569 return "<div class='calendar'>$calendar</div>";
570 }
571
572 # new
573 sub tmpl ($$) {
574 my ($base, $model) = @_;
575 my $page = $base.'/'.'templates/'.$model;
576 my $file = defined srcfile($page, 1) ? '/'.$page : $model;
577 return template($file);
578 }
579 sub date_of_form ($$;%) {
580 my ($form, $prefix, %default) = @_;
581 %default =
582 ( year => 0
583 , month => 1
584 , day => 1
585 , hour => 0
586 , minute => 0
587 , %default
588 );
589 my $date;
590 eval { $date = DateTime->new
591 ( year => ($form->field($prefix.'_year') ne '' ? $form->field($prefix.'_year') : $default{year})
592 , month => ($form->field($prefix.'_month') ne '' ? substr($form->field($prefix.'_month'), 0, 2) : $default{month})
593 , day => ($form->field($prefix.'_day') ne '' ? $form->field($prefix.'_day') : $default{day})
594 , hour => ($form->field($prefix.'_hour') ne '' ? $form->field($prefix.'_hour') : $default{hour})
595 , minute => ($form->field($prefix.'_minute') ne '' ? $form->field($prefix.'_minute') : $default{minute})
596 , second => 0
597 , nanosecond => 0
598 , time_zone => 'local'
599 , locale => $config{locale}
600 )->set_time_zone('floating') };
601 return $date;
602 };
603 sub duration_of_form ($$) {
604 my ($form, $prefix) = @_;
605 my $dur;
606 eval { $dur = DateTime::Duration->new
607 ( years => $form->field($prefix.'_year')
608 , months => $form->field($prefix.'_month')
609 , days => $form->field($prefix.'_day')
610 , weeks => $form->field($prefix.'_week')
611 , hours => $form->field($prefix.'_hour')
612 , minutes => $form->field($prefix.'_minute')
613 , seconds => 0
614 , nanoseconds => 0
615 , end_of_month => 'limit'
616 ) };
617 return $dur;
618 };
619 sub page_of_event ($$$$$) {
620 my ($form, $from_date, $to_date, $name, $base) = @_;
621 my $time = '';
622 if ($form->field('from_hour') ne ''
623 or $form->field('from_minute') ne '') {
624 if ($from_date->hour() == $to_date->hour()
625 and $from_date->minute() == $to_date->minute()) {
626 $time = sprintf('%02dh%02d', $from_date->hour(), $from_date->minute());
627 }
628 else {
629 $time = sprintf('%02dh%02d-%02dh%02d'
630 , $from_date->hour(), $from_date->minute()
631 , $to_date->hour(), $to_date->minute());
632 }
633 }
634 return
635 ( $base
636 . ($base?'/':'').$from_date->year()
637 . '/'.sprintf('%02d', $from_date->month())
638 . '/'.sprintf('%02d', $from_date->day())
639 . '/'. ($time ne '' ? $time . '/' : '')
640 . $name
641 );
642 }
643 sub check_cannewevent ($$$$) {
644 my $dest=shift;
645 my $destfile=shift;
646 my $cgi=shift;
647 my $session=shift;
648
649 # Must be a legal filename.
650 if (IkiWiki::file_pruned($destfile)) {
651 error(sprintf(gettext("illegal name")));
652 }
653 # Must not be a known source file.
654 if (exists $pagesources{$dest}) {
655 error(sprintf(gettext("%s already exists"),
656 htmllink("", "", $dest
657 , linktext => $dest
658 , noimageinline => 1)));
659 }
660 # Must not exist on disk already.
661 if (-l "$config{srcdir}/$destfile" || -e _) {
662 error(sprintf(gettext("%s already exists on disk"), $destfile));
663 }
664
665 # Must be editable.
666 IkiWiki::check_canedit($dest, $cgi, $session);
667
668 my $can_newevent;
669 IkiWiki::run_hooks(can_newevent => sub {
670 return if defined $can_newevent;
671 my $ret=shift->(cgi => $cgi, session => $session, dest => $dest, destfile => $destfile);
672 if (defined $ret) {
673 if ($ret eq "") {
674 $can_newevent=1;
675 }
676 elsif (ref $ret eq 'CODE') {
677 $ret->();
678 $can_newevent=0;
679 }
680 elsif (defined $ret) {
681 error($ret);
682 $can_newevent=0;
683 }
684 }
685 });
686 return defined $can_newevent ? $can_newevent : 1;
687 }
688 sub post_newevent ($$$) {
689 my $cgi=shift;
690 my $session=shift;
691 my $dest=shift;
692
693 IkiWiki::redirect($cgi, urlto($dest));
694 exit;
695 }
696 sub newevent_hook {
697 my %params = @_;
698 my @events = @{$params{events}};
699 my %done = %{$params{done}};
700 my $cgi = $params{cgi};
701 my $session = $params{session};
702 return ()
703 unless @events;
704 my @next;
705 foreach my $event (@events) {
706 unless (exists $done{$event->{page}} && $done{$event->{file}}) {
707 IkiWiki::run_hooks(newevent => sub {
708 push @next, shift->
709 ( cgi => $cgi
710 , event => $event
711 , session => $session
712 );
713 });
714 $done{$event->{page}} = 1;
715 }
716 }
717 push @events, newevent_hook
718 ( cgi => $cgi
719 , done => \%done
720 , events => \@next
721 , session => $session
722 );
723 my %seen; # NOTE: insure unicity
724 return grep { ! $seen{$_->{page}}++ } @events;
725 }
726 sub preview($$$$$) {
727 my ($cgi, $session, $form, $events, $months) = @_;
728 $form->tmpl_param(year => gettext("year"));
729 $form->tmpl_param(month => gettext("month"));
730 $form->tmpl_param(day => gettext("day"));
731 $form->tmpl_param(hour => gettext("hour"));
732 $form->tmpl_param(min => gettext("min"));
733 $form->tmpl_param(dow => gettext("day of week"));
734 $form->tmpl_param(page => gettext("page"));
735 $form->tmpl_param(events => [
736 map {
737 { from_year => $_->{from}->year()
738 , from_month => sprintf('%02d', $_->{from}->month())
739 , from_monthname => $months->{$_->{from}->month()}
740 , from_day => sprintf('%02d', $_->{from}->day())
741 , from_hour => sprintf('%02d', $_->{from}->hour())
742 , from_minute => sprintf('%02d', $_->{from}->minute())
743 , from_dow => $_->{from}->dow()
744 , from_downame => $_->{from}->day_name()
745 , to_year => $_->{to}->year()
746 , to_month => sprintf('%02d', $_->{to}->month())
747 , to_monthname => $months->{$_->{to}->month()}
748 , to_day => sprintf('%02d', $_->{to}->day())
749 , to_hour => sprintf('%02d', $_->{to}->hour())
750 , to_minute => sprintf('%02d', $_->{to}->minute())
751 , to_dow => $_->{to}->dow()
752 , page =>
753 htmllink("", "", $_->{page}
754 , linktext => $_->{page}
755 , noimageinline => 1)
756 }
757 } @$events
758 ]);
759 if (@$events > 0) {
760 my $page = @$events[0];
761 # FROM: editpage.pm
762 my $new = not exists $pagesources{$page};
763 # temporarily record its type
764 my $type = $config{default_pageext};
765 $pagesources{$page} = $page.".".$type if $new;
766 my %wasrendered = map { $_ => 1 } @{$renderedfiles{$page}};
767 my $content = @$events[0]->{content};
768
769 IkiWiki::run_hooks(editcontent => sub {
770 $content = shift->
771 ( cgi => $cgi
772 , content => $content
773 , page => $page
774 , session => $session
775 );
776 });
777 my $preview = IkiWiki::htmlize($page, $page, $type,
778 IkiWiki::linkify($page, $page,
779 IkiWiki::preprocess($page, $page,
780 IkiWiki::filter($page, $page, $content), 0, 1)));
781 IkiWiki::run_hooks(format => sub {
782 $preview = shift->
783 ( content => $preview
784 , page => $page
785 );
786 });
787 $form->tmpl_param("preview", $preview);
788
789 # Previewing may have created files on disk.
790 # Keep a list of these to be deleted later.
791 my %previews = map { $_ => 1 } @{$wikistate{editpage}{previews}};
792 foreach my $f (@{$renderedfiles{$page}}) {
793 $previews{$f} = 1 unless $wasrendered{$f};
794 }
795
796 # Throw out any other state changes made during previewing,
797 # and save the previews list.
798 IkiWiki::loadindex();
799 @{$wikistate{editpage}{previews}} = keys %previews;
800 IkiWiki::saveindex();
801 }
802 else {
803 $form->tmpl_param("preview", gettext("No event"));
804 }
805 }
806 sub create ($$$$$) {
807 my ($event, $cgi, $session, $months, $base) = @_;
808 check_cannewevent
809 ( $event->{page}
810 , $event->{file}
811 , $cgi
812 , $session
813 );
814 my $pageext = $config{default_pageext};
815
816 $config{cgi} = 0; # NOTE: avoid CGI error message
817 eval { writefile($event->{file}, $config{srcdir}, $event->{content}) };
818 if ($config{rcs}) {
819 IkiWiki::rcs_add($event->{file});
820 }
821 # month page
822 my $monthpage =
823 ( $base
824 . ($base?'/':'').$event->{from}->year()
825 . '/'.sprintf('%02d', $event->{from}->month())
826 );
827 my $monthfile = IkiWiki::newpagefile($monthpage, $pageext);
828 if (not exists $pagesources{$monthpage}
829 and not -l $config{srcdir}.'/'.$monthfile
830 and not -e _) {
831 my $tmpl_neweventmonth = tmpl($base, 'neweventmonth.tmpl');
832 $tmpl_neweventmonth->param(base => $base);
833 $tmpl_neweventmonth->param(year => $event->{from}->year());
834 $tmpl_neweventmonth->param(month => sprintf('%02d', $event->{from}->month()));
835 $tmpl_neweventmonth->param(monthname => $months->{$event->{from}->month()});
836 my $content = $tmpl_neweventmonth->output();
837 eval { writefile($monthfile, $config{srcdir}, $content) };
838 if ($config{rcs}) {
839 IkiWiki::rcs_add($monthfile);
840 }
841 }
842 # day page
843 my $daypage =
844 ( $monthpage
845 . '/'.sprintf('%02d', $event->{from}->day())
846 );
847 my $dayfile = IkiWiki::newpagefile($daypage, $pageext);
848 if (not exists $pagesources{$daypage}
849 and not -l $config{srcdir}.'/'.$dayfile
850 and not -e _) {
851 my $tmpl_neweventday = tmpl($base, 'neweventday.tmpl');
852 $tmpl_neweventday->param(base => $base);
853 $tmpl_neweventday->param(year => $event->{from}->year());
854 $tmpl_neweventday->param(month => sprintf('%02d', $event->{from}->month()));
855 $tmpl_neweventday->param(monthname => $months->{$event->{from}->month()});
856 $tmpl_neweventday->param(day => sprintf('%02d', $event->{from}->day()));
857 $tmpl_neweventday->param(dayname => $event->{from}->day_name());
858 my $content = $tmpl_neweventday->output();
859 eval { writefile($dayfile, $config{srcdir}, $content) };
860 if ($config{rcs}) {
861 IkiWiki::rcs_add($dayfile);
862 }
863 }
864 $config{cgi} = 1;
865 }
866 sub sessioncgi ($$) {
867 my ($cgi, $session) = @_;
868 if (defined $cgi->param('do') && $cgi->param('do') eq "newevent") {
869 # TOTRY: decode_cgi_utf8($cgi);
870 my $base = Encode::decode_utf8(URI::Escape::uri_unescape(IkiWiki::possibly_foolish_untaint($cgi->param('base'))));
871 &IkiWiki::check_canedit($base, $cgi, $session);
872 my $page = Encode::decode_utf8(URI::Escape::uri_unescape(IkiWiki::possibly_foolish_untaint($cgi->param('page'))));
873
874 my $now_date = DateTime->now
875 ( time_zone => 'local'
876 , locale => $config{locale}
877 )->set_time_zone('floating');
878 my %dows = map { ($_ => $now_date->{locale}->day_format_wide->[ $_ ]) } (0..6);
879 my %months = map { ($_ => $now_date->{locale}->month_format_wide->[ $_ - 1 ]) } (1..12);
880 my $cgi_date;
881 eval { $cgi_date = DateTime->new
882 ( year => defined $cgi->param("year") ? $cgi->param("year") : $now_date->year()
883 , month => defined $cgi->param("month") ? $cgi->param("month") : $now_date->month()
884 , day => defined $cgi->param("day") ? $cgi->param("day") : $now_date->day()
885 , hour => defined $cgi->param("hour") ? $cgi->param("hour") : $now_date->hour()
886 , minute => defined $cgi->param("minute") ? $cgi->param("minute") : $now_date->minute()
887 , second => 0
888 , nanosecond => 0
889 , time_zone => 'local'
890 , locale => $config{locale}
891 )->set_time_zone('floating') };
892 error(sprintf(gettext("illegal date")))
893 unless $cgi_date;
894
895 my @years = ($cgi_date->year() .. $cgi_date->year()+5);
896 my $week_start_day
897 = (defined $config{week_start_day} and $config{week_start_day} >= 0 and $config{week_start_day} <= 6)
898 ? $config{week_start_day}
899 : 1;
900 my @dow_order = ($week_start_day .. 6, 0 .. $week_start_day-1);
901
902 my $tags = $typedlinks{$page}{tag};
903 my $buttons = [qw{Preview Create}];
904 my ($from_date, $to_date, $end_date, $inc_dur);
905 my $form = CGI::FormBuilder->new
906 ( action => IkiWiki::cgiurl()
907 , charset => "utf-8"
908 , fields => [qw{
909 do base
910 from_date from_year from_month from_day from_hour from_minute
911 to_date to_year to_month to_day to_hour to_minute
912 inc_dur inc_year inc_month inc_week inc_day inc_hour inc_minute
913 end_times end_date end_year end_month end_day end_hour end_minute
914 dom name content
915 }]
916 , header => 0
917 , javascript => 0
918 , messages =>
919 {
920 # form_required_text => 'form_required_text'
921 # , form_invalid_text => 'form_invalid_text'
922 # , form_invalid_file => 'form_invalid_file'
923 # , form_invalid_input => gettext('allowed characters: ').$config{wiki_file_chars}
924 form_invalid_select => gettext('invalid selection')
925 }
926 , method => 'POST'
927 , name => "newevent"
928 , stylesheet => 1
929 , params => $cgi
930 , required => [qw{do base year month day name from_date to_date end_date inc_dur}]
931 , submit => [qw{Preview Create}]
932 , title => gettext("newevent")
933 , template => { template("newevent.tmpl") }
934 , validate =>
935 { from_date => { perl => sub {
936 my (undef, $form) = @_;
937 $from_date = date_of_form($form, 'from')
938 unless defined $from_date;
939 defined $from_date
940 } }
941 , to_date => { perl => sub {
942 my (undef, $form) = @_;
943 $from_date = date_of_form($form, 'from')
944 unless defined $from_date;
945 if (defined $from_date) {
946 $to_date = date_of_form($form, 'to'
947 , year => $from_date->year()
948 , month => $from_date->month()
949 , day => $from_date->day()
950 , hour => $from_date->hour()
951 , minute => $from_date->minute());
952 defined $to_date
953 and (DateTime->compare($from_date, $to_date) <= 0)
954 }
955 else {return 0;}
956 } }
957 , end_date => { perl => sub {
958 my (undef, $form) = @_;
959 if ( $form->field('end_year') ne ''
960 or $form->field('end_month') ne ''
961 or $form->field('end_day') ne '' ) {
962 $from_date = date_of_form($form, 'from')
963 unless defined $from_date;
964 if (defined $from_date) {
965 $end_date = date_of_form($form, 'end'
966 , year => $from_date->year()
967 , month => $from_date->month()
968 , day => $from_date->day()
969 , hour => $from_date->hour()
970 , minute => $from_date->minute());
971 (defined $from_date and defined $end_date
972 and DateTime->compare($from_date, $end_date) <= 0)
973 }
974 else {return 0;}
975 }
976 else {
977 1;
978 }
979 } }
980 , name => '/^.+$/'
981 , base => '/^.*$/'
982 , end_times => sub { $_[0] =~ m/^\d+$/ and $_[0] >= 0 }
983 , inc_year => sub { $_[0] =~ m/^\d+$/ and $_[0] >= 0 }
984 , inc_month => sub { $_[0] =~ m/^\d+$/ and $_[0] >= 0 }
985 , inc_week => sub { $_[0] =~ m/^\d+$/ and $_[0] >= 0 }
986 , inc_day => sub { $_[0] =~ m/^\d+$/ and $_[0] >= 0 }
987 , inc_hour => sub { $_[0] =~ m/^\d+$/ and $_[0] >= 0 }
988 , inc_minute => sub { $_[0] =~ m/^\d+$/ and $_[0] >= 0 }
989 , inc_dur => sub {
990 my (undef, $form) = @_;
991 $inc_dur = duration_of_form($form, 'inc');
992 defined $inc_dur
993 and ($inc_dur->is_positive() or $inc_dur->is_zero());
994 }
995 }
996 );
997 $base = $form->field('base') ? $form->field('base') : $base;
998 $form->title(sprintf(gettext("creating new events"), pagetitle(IkiWiki::basename($page))));
999 $form->field(name => "do", type => "hidden", value => 'newevent', force => 1);
1000 $form->field(name => "base", type => "hidden", force => 1 , value => $base);
1001 $form->field(name => "from_date", type => "hidden", value => '1', force => 1);
1002 $form->field(name => "to_date", type => "hidden", value => '1', force => 1);
1003 $form->field(name => "end_date", type => "hidden", value => '1', force => 1);
1004 $form->field(name => "inc_dur", type => "hidden", value => '1', force => 1);
1005 $form->field(name => "from_year", type => 'select', value => $cgi_date->year(), options => \@years);
1006 $form->field(name => "from_month", type => 'select'
1007 , value => sprintf("%02d", $cgi_date->month()).' - '.$months{$cgi_date->month()}
1008 , options => [map { sprintf("%02d", $_).' - '.$months{$_} } (1..12)]);
1009 $form->field(name => "from_day", type => 'select'
1010 , value => sprintf("%02d", $cgi_date->day())
1011 , options => \@days);
1012 $form->field(name => "from_hour", type => 'select', value => '', options => \@hours);
1013 $form->field(name => "from_minute", type => 'select', value => '', options => \@minutes);
1014 $form->field(name => "name", type => 'text', size => 60, value => gettext('New event'));
1015 $form->field(name => "to_year", type => 'select', value => '', options => \@years);
1016 $form->field(name => "to_month", type => 'select'
1017 , value => ''
1018 , options => [map { sprintf("%02d", $_).' - '.$months{$_} } (1..12)]);
1019 $form->field(name => "to_day", type => 'select'
1020 , value => ''
1021 , options => \@days);
1022 $form->field(name => "to_hour", type => 'select', value => '', options => \@hours);
1023 $form->field(name => "to_minute", type => 'select', value => '', options => \@minutes);
1024 $form->field(name => "end_year", type => 'select', value => '', options => \@years);
1025 $form->field(name => "end_month", type => 'select', value => ''
1026 , options => [map { sprintf("%02d", $_).' - '.$months{$_} } (1..12)]);
1027 $form->field(name => "end_day", type => 'select', value => '', options => \@days);
1028 $form->field(name => "end_hour", type => 'select', value => '', options => \@hours);
1029 $form->field(name => "end_minute", type => 'select', value => '', options => \@minutes);
1030 $form->field(name => "end_times", type => 'text', value => '0', size => 2);
1031 $form->field(name => "inc_year", type => 'text', value => '0', size => 2);
1032 $form->field(name => "inc_month", type => 'text', value => '0', size => 2);
1033 $form->field(name => "inc_week", type => 'text', value => '0', size => 2);
1034 $form->field(name => "inc_day", type => 'text', value => '0', size => 2);
1035 $form->field(name => "inc_hour", type => 'text', value => '0', size => 2);
1036 $form->field(name => "inc_minute", type => 'text', value => '0', size => 2);
1037 my $tmpl_neweventcontent = tmpl($base, 'neweventcontent.tmpl');
1038 $tmpl_neweventcontent->param(title => gettext('Title of the event'));
1039 $tmpl_neweventcontent->param(tags => [map {{name => $_}} (sort keys %$tags)]);
1040 $form->field(name => "content", type => "textarea", size => 30, rows => 20, cols => 80
1041 , value => $tmpl_neweventcontent->output());
1042 $form->field(name => "dom", type => 'select', multiple => 1, size => 35
1043 , options => [map { my $n = $_; map {($n.' '.$dows{$_})} (0..6)} ('1°', '2°', '3°', '4°', '5°')]);
1044
1045 IkiWiki::decode_form_utf8($form);
1046 IkiWiki::run_hooks(formbuilder_setup => sub {
1047 shift->(form => $form, cgi => $cgi, session => $session, buttons => $buttons);
1048 });
1049 IkiWiki::decode_form_utf8($form);
1050
1051 if (($form->submitted eq 'Create' || $form->submitted eq 'Preview') && $form->validate) {
1052 #IkiWiki::checksessionexpiry($cgi, $session, $cgi->param('sid'));
1053 $base
1054 = $form->field('base')
1055 ? $form->field('base')
1056 : (defined $config{base} ? $config{base} : gettext('Agenda'));
1057 my $end_times
1058 = $form->field('end_times') == 0
1059 ? undef : $form->field('end_times');
1060 my $dom;
1061 foreach ($form->field('dom')) {
1062 $dom = {} if not defined $dom;
1063 $dom->{$_} = 1;
1064 }
1065 my $name = $form->field('name');
1066 $name = IkiWiki::possibly_foolish_untaint(IkiWiki::titlepage($name));
1067 # NOTE: these untaints are safe because of the checks
1068 # performed in check_cannewevent later.
1069 my $content = $form->field('content');
1070 $content =~ s/\r\n/\n/gs;
1071 $content =~ s/\n$//s;
1072
1073 # Queue of event creations to perfom.
1074 my @events = ();
1075 my $events_try = 0;
1076 my $events_max
1077 = defined $config{newevent_max_per_commit}
1078 ? $config{newevent_max_per_commit} : (2 * 365) ;
1079 my $pageext = $config{default_pageext};
1080 while (++$events_try <= $events_max
1081 and (not defined $end_times or --$end_times >= 0)
1082 and (not defined $end_date or DateTime->compare($from_date, $end_date) <= 0)) {
1083 my $dest = page_of_event($form, $from_date, $to_date, $name, $base);
1084 my $week = $from_date->weekday_of_month();
1085 my $day = $now_date->{locale}->day_format_wide->[$from_date->day_of_week()-1];
1086 if (not defined $dom or exists $dom->{"$week° $day"}) {
1087 push @events,
1088 { page => $dest
1089 , file => IkiWiki::newpagefile($dest, $pageext)
1090 , from => $from_date
1091 , to => $to_date
1092 , name => $name
1093 };
1094 }
1095 last unless defined $inc_dur and $inc_dur->is_positive();
1096 $from_date = $from_date->clone->add_duration($inc_dur);
1097 $to_date = $to_date->clone->add_duration($inc_dur);
1098 }
1099 error("events try per commit overflow: $events_max")
1100 unless $events_try <= $events_max;
1101 my $tmpl_neweventpage = tmpl($base, 'neweventpage.tmpl');
1102 my $i = 0;
1103 foreach (@events) {
1104 $tmpl_neweventpage->clear_params();
1105 $tmpl_neweventpage->param(content => $content);
1106 $tmpl_neweventpage->param(page => $_->{page});
1107 $tmpl_neweventpage->param(event => $i);
1108 $tmpl_neweventpage->param("event_first" => 1)
1109 if $i == 0;
1110 $tmpl_neweventpage->param("event_last" => 1)
1111 if $i == @events - 1;
1112 $tmpl_neweventpage->param(events => \@events);
1113 $tmpl_neweventpage->param(from_date => "$_->{from}");
1114 $tmpl_neweventpage->param(name => $_->{name});
1115 $tmpl_neweventpage->param(to_date => "$_->{to}");
1116 $_->{content} = $tmpl_neweventpage->output();
1117 $i++;
1118 }
1119 if ($form->submitted eq 'Create') {
1120 @events = newevent_hook
1121 ( cgi => $cgi
1122 , done => {}
1123 , events => \@events
1124 , session => $session
1125 );
1126 require IkiWiki::Render;
1127 if ($config{rcs}) {
1128 IkiWiki::disable_commit_hook()
1129 }
1130 foreach my $event (@events) {
1131 create($event, $cgi, $session, \%months, $base);
1132 }
1133 if ($config{rcs}) {
1134 IkiWiki::rcs_commit_staged
1135 ( message => sprintf(gettext("new event"))
1136 , session => $session );
1137 IkiWiki::enable_commit_hook();
1138 IkiWiki::rcs_update();
1139 }
1140 IkiWiki::refresh();
1141 IkiWiki::saveindex();
1142
1143 post_newevent($cgi, $session, (defined $events[0] ? $events[0]->{page} : ''));
1144 }
1145 elsif ($form->submitted eq 'Preview') {
1146 preview($cgi, $session, $form, \@events, \%months);
1147 IkiWiki::showform($form, $buttons, $session, $cgi);
1148 }
1149 }
1150 else {
1151 IkiWiki::showform($form, $buttons, $session, $cgi);
1152 }
1153
1154 exit 0;
1155 }
1156 }
1157
1158 1;