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