1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle 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, or
7// (at your option) any later version.
8//
9// Moodle 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 Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
17/**
18 * Contains event class for displaying the month view.
19 *
20 * @package   core_calendar
21 * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
22 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24
25namespace core_calendar\external;
26
27defined('MOODLE_INTERNAL') || die();
28
29use core\external\exporter;
30use renderer_base;
31use moodle_url;
32
33/**
34 * Class for displaying the month view.
35 *
36 * @package   core_calendar
37 * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
38 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39 */
40class month_exporter extends exporter {
41
42    /**
43     * @var \calendar_information $calendar The calendar to be rendered.
44     */
45    protected $calendar;
46
47    /**
48     * @var int $firstdayofweek The first day of the week.
49     */
50    protected $firstdayofweek;
51
52    /**
53     * @var moodle_url $url The URL for the events page.
54     */
55    protected $url;
56
57    /**
58     * @var bool $includenavigation Whether navigation should be included on the output.
59     */
60    protected $includenavigation = true;
61
62    /**
63     * @var bool $initialeventsloaded Whether the events have been loaded for this month.
64     */
65    protected $initialeventsloaded = true;
66
67    /**
68     * @var bool $showcoursefilter Whether to render the course filter selector as well.
69     */
70    protected $showcoursefilter = false;
71
72    /**
73     * Constructor for month_exporter.
74     *
75     * @param \calendar_information $calendar The calendar being represented
76     * @param \core_calendar\type_base $type The calendar type (e.g. Gregorian)
77     * @param array $related The related information
78     */
79    public function __construct(\calendar_information $calendar, \core_calendar\type_base $type, $related) {
80        $this->calendar = $calendar;
81        $this->firstdayofweek = $type->get_starting_weekday();
82
83        $this->url = new moodle_url('/calendar/view.php', [
84                'view' => 'month',
85                'time' => $calendar->time,
86            ]);
87
88        if ($this->calendar->course && SITEID !== $this->calendar->course->id) {
89            $this->url->param('course', $this->calendar->course->id);
90        } else if ($this->calendar->categoryid) {
91            $this->url->param('category', $this->calendar->categoryid);
92        }
93
94        $related['type'] = $type;
95
96        $data = [
97            'url' => $this->url->out(false),
98        ];
99
100        parent::__construct($data, $related);
101    }
102
103    protected static function define_properties() {
104        return [
105            'url' => [
106                'type' => PARAM_URL,
107            ],
108        ];
109    }
110
111    /**
112     * Return the list of additional properties.
113     *
114     * @return array
115     */
116    protected static function define_other_properties() {
117        return [
118            'courseid' => [
119                'type' => PARAM_INT,
120            ],
121            'categoryid' => [
122                'type' => PARAM_INT,
123                'optional' => true,
124                'default' => 0,
125            ],
126            'filter_selector' => [
127                'type' => PARAM_RAW,
128                'optional' => true,
129            ],
130            'weeks' => [
131                'type' => week_exporter::read_properties_definition(),
132                'multiple' => true,
133            ],
134            'daynames' => [
135                'type' => day_name_exporter::read_properties_definition(),
136                'multiple' => true,
137            ],
138            'view' => [
139                'type' => PARAM_ALPHA,
140            ],
141            'date' => [
142                'type' => date_exporter::read_properties_definition(),
143            ],
144            'periodname' => [
145                // Note: We must use RAW here because the calendar type returns the formatted month name based on a
146                // calendar format.
147                'type' => PARAM_RAW,
148            ],
149            'includenavigation' => [
150                'type' => PARAM_BOOL,
151                'default' => true,
152            ],
153            // Tracks whether the first set of events have been loaded and provided
154            // to the exporter.
155            'initialeventsloaded' => [
156                'type' => PARAM_BOOL,
157                'default' => true,
158            ],
159            'previousperiod' => [
160                'type' => date_exporter::read_properties_definition(),
161            ],
162            'previousperiodlink' => [
163                'type' => PARAM_URL,
164            ],
165            'previousperiodname' => [
166                // Note: We must use RAW here because the calendar type returns the formatted month name based on a
167                // calendar format.
168                'type' => PARAM_RAW,
169            ],
170            'nextperiod' => [
171                'type' => date_exporter::read_properties_definition(),
172            ],
173            'nextperiodname' => [
174                // Note: We must use RAW here because the calendar type returns the formatted month name based on a
175                // calendar format.
176                'type' => PARAM_RAW,
177            ],
178            'nextperiodlink' => [
179                'type' => PARAM_URL,
180            ],
181            'larrow' => [
182                // The left arrow defined by the theme.
183                'type' => PARAM_RAW,
184            ],
185            'rarrow' => [
186                // The right arrow defined by the theme.
187                'type' => PARAM_RAW,
188            ],
189            'defaulteventcontext' => [
190                'type' => PARAM_INT,
191                'default' => 0,
192            ],
193        ];
194    }
195
196    /**
197     * Get the additional values to inject while exporting.
198     *
199     * @param renderer_base $output The renderer.
200     * @return array Keys are the property names, values are their values.
201     */
202    protected function get_other_values(renderer_base $output) {
203        $previousperiod = $this->get_previous_month_data();
204        $nextperiod = $this->get_next_month_data();
205        $date = $this->related['type']->timestamp_to_date_array($this->calendar->time);
206
207        $nextperiodlink = new moodle_url($this->url);
208        $nextperiodlink->param('time', $nextperiod[0]);
209
210        $previousperiodlink = new moodle_url($this->url);
211        $previousperiodlink->param('time', $previousperiod[0]);
212
213        $return = [
214            'courseid' => $this->calendar->courseid,
215            'weeks' => $this->get_weeks($output),
216            'daynames' => $this->get_day_names($output),
217            'view' => 'month',
218            'date' => (new date_exporter($date))->export($output),
219            'periodname' => userdate($this->calendar->time, get_string('strftimemonthyear')),
220            'previousperiod' => (new date_exporter($previousperiod))->export($output),
221            'previousperiodname' => userdate($previousperiod[0], get_string('strftimemonthyear')),
222            'previousperiodlink' => $previousperiodlink->out(false),
223            'nextperiod' => (new date_exporter($nextperiod))->export($output),
224            'nextperiodname' => userdate($nextperiod[0], get_string('strftimemonthyear')),
225            'nextperiodlink' => $nextperiodlink->out(false),
226            'larrow' => $output->larrow(),
227            'rarrow' => $output->rarrow(),
228            'includenavigation' => $this->includenavigation,
229            'initialeventsloaded' => $this->initialeventsloaded,
230        ];
231
232        if ($this->showcoursefilter) {
233            $return['filter_selector'] = $this->get_course_filter_selector($output);
234        }
235
236        if ($context = $this->get_default_add_context()) {
237            $return['defaulteventcontext'] = $context->id;
238        }
239
240        if ($this->calendar->categoryid) {
241            $return['categoryid'] = $this->calendar->categoryid;
242        }
243
244        return $return;
245    }
246
247    /**
248     * Get the course filter selector.
249     *
250     * @param renderer_base $output
251     * @return string The html code for the course filter selector.
252     */
253    protected function get_course_filter_selector(renderer_base $output) {
254        $content = '';
255        $content .= $output->course_filter_selector($this->url, '', $this->calendar->course->id);
256
257        return $content;
258    }
259
260    /**
261     * Get the list of day names for display, re-ordered from the first day
262     * of the week.
263     *
264     * @param   renderer_base $output
265     * @return  day_name_exporter[]
266     */
267    protected function get_day_names(renderer_base $output) {
268        $weekdays = $this->related['type']->get_weekdays();
269        $daysinweek = count($weekdays);
270
271        $daynames = [];
272        for ($i = 0; $i < $daysinweek; $i++) {
273            // Bump the currentdayno and ensure it loops.
274            $dayno = ($i + $this->firstdayofweek + $daysinweek) % $daysinweek;
275            $dayname = new day_name_exporter($dayno, $weekdays[$dayno]);
276            $daynames[] = $dayname->export($output);
277        }
278
279        return $daynames;
280    }
281
282    /**
283     * Get the list of week days, ordered into weeks and padded according
284     * to the value of the first day of the week.
285     *
286     * @param renderer_base $output
287     * @return array The list of weeks.
288     */
289    protected function get_weeks(renderer_base $output) {
290        $weeks = [];
291        $alldays = $this->get_days();
292
293        $daysinweek = count($this->related['type']->get_weekdays());
294
295        // Calculate which day number is the first, and last day of the week.
296        $firstdayofweek = $this->firstdayofweek;
297
298        // The first week is special as it may have padding at the beginning.
299        $day = reset($alldays);
300        $firstdayno = $day['wday'];
301
302        $prepadding = ($firstdayno + $daysinweek - $firstdayofweek) % $daysinweek;
303        $daysinfirstweek = $daysinweek - $prepadding;
304        $days = array_slice($alldays, 0, $daysinfirstweek);
305        $week = new week_exporter($this->calendar, $days, $prepadding, ($daysinweek - count($days) - $prepadding), $this->related);
306        $weeks[] = $week->export($output);
307
308        // Now chunk up the remaining day. and turn them into weeks.
309        $daychunks = array_chunk(array_slice($alldays, $daysinfirstweek), $daysinweek);
310        foreach ($daychunks as $days) {
311            $week = new week_exporter($this->calendar, $days, 0, ($daysinweek - count($days)), $this->related);
312            $weeks[] = $week->export($output);
313        }
314
315        return $weeks;
316    }
317
318    /**
319     * Get the list of days with the matching date array.
320     *
321     * @return array
322     */
323    protected function get_days() {
324        $date = $this->related['type']->timestamp_to_date_array($this->calendar->time);
325        $monthdays = $this->related['type']->get_num_days_in_month($date['year'], $date['mon']);
326
327        $days = [];
328        for ($dayno = 1; $dayno <= $monthdays; $dayno++) {
329            // Get the gregorian representation of the day.
330            $timestamp = $this->related['type']->convert_to_timestamp($date['year'], $date['mon'], $dayno);
331
332            $days[] = $this->related['type']->timestamp_to_date_array($timestamp);
333        }
334
335        return $days;
336    }
337
338    /**
339     * Returns a list of objects that are related.
340     *
341     * @return array
342     */
343    protected static function define_related() {
344        return [
345            'events' => '\core_calendar\local\event\entities\event_interface[]',
346            'cache' => '\core_calendar\external\events_related_objects_cache',
347            'type' => '\core_calendar\type_base',
348        ];
349    }
350
351    /**
352     * Get the current month timestamp.
353     *
354     * @return int The month timestamp.
355     */
356    protected function get_month_data() {
357        $date = $this->related['type']->timestamp_to_date_array($this->calendar->time);
358        $monthtime = $this->related['type']->convert_to_gregorian($date['year'], $date['month'], 1);
359
360        return make_timestamp($monthtime['year'], $monthtime['month']);
361    }
362
363    /**
364     * Get the previous month timestamp.
365     *
366     * @return int The previous month timestamp.
367     */
368    protected function get_previous_month_data() {
369        $type = $this->related['type'];
370        $date = $type->timestamp_to_date_array($this->calendar->time);
371        list($date['mon'], $date['year']) = $type->get_prev_month($date['year'], $date['mon']);
372        $time = $type->convert_to_timestamp($date['year'], $date['mon'], 1);
373
374        return $type->timestamp_to_date_array($time);
375    }
376
377    /**
378     * Get the next month timestamp.
379     *
380     * @return int The next month timestamp.
381     */
382    protected function get_next_month_data() {
383        $type = $this->related['type'];
384        $date = $type->timestamp_to_date_array($this->calendar->time);
385        list($date['mon'], $date['year']) = $type->get_next_month($date['year'], $date['mon']);
386        $time = $type->convert_to_timestamp($date['year'], $date['mon'], 1);
387
388        return $type->timestamp_to_date_array($time);
389    }
390
391    /**
392     * Set whether the navigation should be shown.
393     *
394     * @param   bool    $include
395     * @return  $this
396     */
397    public function set_includenavigation($include) {
398        $this->includenavigation = $include;
399
400        return $this;
401    }
402
403    /**
404     * Set whether the initial events have already been loaded and
405     * provided to the exporter.
406     *
407     * @param   bool    $loaded
408     * @return  $this
409     */
410    public function set_initialeventsloaded(bool $loaded) {
411        $this->initialeventsloaded = $loaded;
412
413        return $this;
414    }
415
416    /**
417     * Set whether the course filter selector should be shown.
418     *
419     * @param   bool    $show
420     * @return  $this
421     */
422    public function set_showcoursefilter(bool $show) {
423        $this->showcoursefilter = $show;
424
425        return $this;
426    }
427
428    /**
429     * Get the default context for use when adding a new event.
430     *
431     * @return null|\context
432     */
433    protected function get_default_add_context() {
434        if (calendar_user_can_add_event($this->calendar->course)) {
435            return \context_course::instance($this->calendar->course->id);
436        }
437
438        return null;
439    }
440}
441