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