Newer
Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
<?php
/**
* CalendarDateAssignment.class.php - Model class for calendar date assignments.
*
* CalendarDateAssignment represents the assignment of a calendar date
* to a specific calendar. The calendar is represented by a range-ID
* since it can be a personal calendar, course calendar or institute
* calendar.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* @author Moritz Strohm <strohm@data-quest.de>
* @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
* @category Stud.IP
* @since 5.5
*
* @property string range_id The range-ID for the assignment.
* @property string calendar_date_id The ID of the calendar date for the assignment.
* @property string participation The participation status of the receiver (range_id).
* This column is an enum with the following values:
* - empty string: Participation status is unknown.
* - "ACCEPTED": The calendar owner accepted the date.
* - "DECLINED": The calendar owner declined the date.
* - "ACKNOWLEDGED": The calendar owner only acknowledged that the date exists
* but doesn't necessarily participate in it.
* @property string mkdate The creation date of the assignment.
* @property string chdate The modification date of the assignment.
* @property CalendarDate|null calendar_date The associated calendar date object.
*/
class CalendarDateAssignment extends SimpleORMap implements Event
{
/**
* @var bool This attribute allows the suppression of automatic mail sending
* when storing or deleting the calendar date assignment.
* By default, mails are sent.
*/
public $suppress_mails = false;
protected static function configure($config = [])
{
$config['db_table'] = 'calendar_date_assignments';
$config['belongs_to']['calendar_date'] = [
'class_name' => CalendarDate::class,
'foreign_key' => 'calendar_date_id',
'assoc_func' => 'find'
];
$config['belongs_to']['user'] = [
'class_name' => User::class,
'foreign_key' => 'range_id',
'assoc_func' => 'find'
];
$config['belongs_to']['course'] = [
'class_name' => Course::class,
'foreign_key' => 'range_id',
'assoc_func' => 'find'
];
$config['registered_callbacks']['after_create'][] = 'cbSendNewDateMail';
$config['registered_callbacks']['after_delete'][] = 'cbSendDateDeletedMail';
parent::configure($config);
}
public function cbSendNewDateMail()
{
if ($this->suppress_mails) {
return;
}
if ($this->range_id === $this->calendar_date->editor_id) {
return;
}
if (!$this->calendar_date || !$this->user) {
//Wrong calendar range (not a user) or invalid data set.
return;
}
$template_factory = new Flexi_TemplateFactory($GLOBALS['STUDIP_BASE_PATH'] . '/locale/');
setTempLanguage($this->range_id);
$lang_path = getUserLanguagePath($this->range_id);
$template = $template_factory->open($lang_path . '/LC_MAILS/date_created.php');
$template->set_attribute('date', $this->calendar_date);
$template->set_attribute('receiver', $this->user);
$mail_text = $template->render();
Message::send(
'____%system%____',
[$this->user->username],
sprintf(_('%s hat einen Termin im Kalender eingetragen'), $this->calendar_date->editor->getFullName()),
$mail_text
);
restoreLanguage();
}
public function cbSendDateDeletedMail()
{
if ($this->suppress_mails) {
return;
}
Moritz Strohm
committed
$actor = User::findCurrent() ?? $this->calendar_date->editor;
if ($this->range_id === $actor->id) {
//The user who deleted the date shall not get notified about this.
return;
}
if (!$this->calendar_date || !$this->user) {
//Wrong calendar range (not a user) or invalid data set.
return;
}
$template_factory = new Flexi_TemplateFactory($GLOBALS['STUDIP_BASE_PATH'] . '/locale/');
setTempLanguage($this->range_id);
$lang_path = getUserLanguagePath($this->range_id);
$template = $template_factory->open($lang_path . '/LC_MAILS/date_deleted.php');
$template->set_attribute('date', $this->calendar_date);
Moritz Strohm
committed
$template->set_attribute('actor', $actor);
$template->set_attribute('receiver', $this->user);
$mail_text = $template->render();
Message::send(
'____%system%____',
[$this->user->username],
Moritz Strohm
committed
sprintf(_('%s hat einen Termin im Kalender gelöscht'), $actor->getFullName()),
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
$mail_text
);
restoreLanguage();
}
/**
* Sends the participation status of the calendar the date
* is assigned to. This is only done for user calendars
* and not for course calendars.
*
* @return void
*/
public function sendParticipationStatus() : void
{
if (!($this->user instanceof User)) {
//The calendar date is assigned to a course calendar.
return;
}
if (!$this->participation || $this->participation === 'ACKNOWLEDGED') {
//Nothing shall be done in these two cases.
return;
}
if (empty($this->calendar_date->author->username)) {
//The calendar date has no author.
return;
}
if ($this->range_id === $this->calendar_date->author_id) {
//The author of the date changed their participation status.
//So they know what they did and do not have to be notified.
return;
}
$template_factory = new Flexi_TemplateFactory($GLOBALS['STUDIP_BASE_PATH'] . '/locale/');
setTempLanguage($this->range_id);
$lang_path = getUserLanguagePath($this->range_id);
$template = $template_factory->open($lang_path . '/LC_MAILS/date_participation.php');
$template->set_attribute('date_assignment', $this);
$mail_text = $template->render();
$subject = '';
if ($this->participation === 'ACCEPTED') {
$subject = sprintf(
_('%1$s hat Ihren Termin am %2$s angenommen'),
$this->user->getFullName(),
date('d.m.Y', $this->calendar_date->begin)
);
} elseif ($this->participation === 'DECLINED') {
$subject = sprintf(
_('%1$s hat Ihren Termin am %2$s abgelehnt'),
$this->user->getFullName(),
date('d.m.Y', $this->calendar_date->begin)
);
}
Message::send(
'____%system%____',
[$this->calendar_date->author->username],
$subject,
$mail_text
);
restoreLanguage();
}
/**
* Retrieves calendar dates inside a specified time range that are present in the calendar of a
* course or user. They can additionally be filtered by the access level and declined events
* can be filtered out, too.
*
* @param DateTime $begin The beginning of the time range.
*
* @param DateTime $end The end of the time range.
*
* @param string $range_id The ID of the course or user whose calendar dates shall be retrieved.
*
* @param array $access_levels The access level filter: Only include calendar dates that have one of the
* access levels in the list.
*
* @param bool $with_declined Include declined calendar dates (true) or filter them out (false).
* Defaults to false.
*
* @return CalendarDateAssignment[] A list of calendar date assignments in the time range that match the filters.
*/
public static function getEvents(
DateTime $begin,
DateTime $end,
string $range_id,
array $access_levels = ['PUBLIC', 'PRIVATE', 'CONFIDENTIAL'],
bool $with_declined = false
) : array
{
Peter Thienel
committed
// Always use the timezone of the server:
$local_timezone = (new DateTime())->getTimezone();
$begin->setTimezone($local_timezone);
$end->setTimezone($local_timezone);
Peter Thienel
committed
// one whole day as minimum (begin and end time stamp at the same day)
$begin->modify('midnight');
$end->modify('tomorrow -1 second');
$sql = "JOIN `calendar_dates`
ON calendar_date_id = `calendar_dates`.`id`
WHERE

André Noack
committed
`calendar_date_assignments`.`range_id` = :range_id
AND
`access` IN ( :access_levels ) ";
if (!$with_declined) {
$sql .= "AND `calendar_date_assignments`.`participation` <> 'DECLINED' ";
}

André Noack
committed
$sql_single = $sql . " AND
`calendar_dates`.`begin` < :end AND :begin < `calendar_dates`.`end`
";

André Noack
committed
$events = self::findBySql($sql_single, [
'range_id' => $range_id,
'begin' => $begin->getTimestamp(),
'end' => $end->getTimestamp(),
'access_levels' => $access_levels
]);

André Noack
committed
Peter Thienel
committed
$sql_repetition = $sql . " AND `calendar_dates`.`begin` < :end AND `calendar_dates`.`repetition_type` IN ('DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY')

André Noack
committed
AND `calendar_dates`.`repetition_end` > :begin
";
$events = array_merge($events, self::findBySql($sql_repetition, [
'range_id' => $range_id,
'begin' => $begin->getTimestamp(),
'end' => $end->getTimestamp(),
'access_levels' => $access_levels
]));
$m_start = clone $begin;
$m_end = clone $end;
$events_created = [];
while ($m_start < $m_end) {
foreach ($events as $event) {
$e_start = clone $event->getBegin();
$e_end = clone $event->getEnd();
$e_expire = $event->getExpire();
$cal_start = DateTimeImmutable::createFromMutable($m_start);
Peter Thienel
committed
$cal_end = DateTimeImmutable::createFromMutable($m_start)->modify('tomorrow -1 second');
$cal_noon = $cal_start->modify('noon');
// single events or first event
if (
($e_start >= $cal_start && $e_end <= $cal_end)
|| ($e_start >= $cal_start && $e_start <= $cal_end)
|| ($e_start < $cal_start && $e_end > $cal_end)
|| ($e_end > $cal_start && $e_start <= $cal_end)
) {
// exception for first event or single event
Peter Thienel
committed
if (!$event->calendar_date->exceptions->findOneBy('date', $cal_start->format('Y-m-d'))
&& !isset($events_created[$event->calendar_date->id])) {
$events_created[$event->calendar_date->id . '_' . $event->calendar_date->begin] = $event;
Peter Thienel
committed
$events_created = array_merge($events_created, self::getRepetition($event, $cal_noon));
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
}
}
$m_start->modify('+1 day');
}
return $events_created;
}
private static function getRepetition(
CalendarDateAssignment $date,
DateTimeImmutable $cal_noon,
bool $calc_prev = true
): array
{
$rep_dates = [];
$ts = $date->getNoonDate();
if ($cal_noon >= $ts) {
if ($date->isRepeatedAtDate($cal_noon)) {
$rep_dates = array_merge($rep_dates, self::createRecurrentDate($date, $cal_noon));
}
if ($calc_prev) {
$rep_noon = $cal_noon->modify(sprintf('-%s days', $date->getDurationDays()));
$rep_dates = array_merge(
$rep_dates,
self::getRepetition(
$date,
$rep_noon,
false
)
);
}
}
return $rep_dates;
}
private function isRepeatedAtDate(DateTimeImmutable $cal_date): bool
{
$ts = $this->getNoonDate();
$pos = 1;
switch ($this->getRepetitionType()) {
case 'DAILY':
$pos = $cal_date->diff($ts)->days % $this->calendar_date->interval;
break;
case 'WEEKLY':
$cal_ts = $cal_date->modify('monday this week noon');
if ($cal_date >= $this->getBegin()) {
$pos = $cal_ts->diff($ts)->days % ($this->calendar_date->interval * 7);
if (
$pos === 0
&& strpos($this->calendar_date->days, $cal_date->format('N')) === false
) {
$pos = 1;
}
}
break;
case 'MONTHLY':
$cal_ts = $cal_date->modify('first day of this month noon');
$diff = $cal_ts->diff($ts);
$pos = ($diff->m + $diff->y * 12) % $this->calendar_date->interval;
if ($pos === 0) {
if (strlen($this->calendar_date->days)) {
$cal_ts_dom = $cal_ts->modify(sprintf('%s %s of this month noon',
$this->calendar_date->getOrdinalName(),
$this->calendar_date->getWeekdayName()));
if ($cal_ts_dom != $cal_date->setTime(12, 0)) {
$pos = 1;
}
} elseif ($this->calendar_date->offset !== $cal_date->format('j')) {
$pos = 1;
}
}
break;
case 'YEARLY':
$cal_ts = $cal_date->modify('first day of this year noon');
$diff = $cal_ts->diff($ts);
$pos = $diff->y % $this->calendar_date->interval;
if ($pos === 0) {
if (strlen($this->calendar_date->days)) {
$ts_doy = $ts->modify(sprintf('%s %s of %s-%s noon',
$this->calendar_date->getOrdinalName(),
$this->calendar_date->getWeekdayName(),
$cal_date->format('Y'),
$this->calendar_date->month));
if ($ts_doy->format('n-j') !== $cal_date->format('n-j')) {
$pos = 1;
}
} elseif (
$cal_date->format('n-j') !== sprintf(
'%s-%s',
$this->calendar_date->month,
$this->calendar_date->offset
)
) {
$pos = 1;
}
}
break;
default:
$pos = 1;
}
//Also check for exceptions before returning:
return $pos === 0
&& !$this->calendar_date->exceptions->findOneBy(
'date',
$cal_date->format('Y-m-d'));
}
private static function createRecurrentDate(
CalendarDateAssignment $date,
DateTimeImmutable $date_time
) : array
{
$date_begin = $date->getBegin();
$date_end = $date->getEnd();
$rec_date = clone $date;
$time_begin = $date_begin->format('H:i:s');
$time_end = $date_end->format('H:i:s');
$rec_date_begin = $date_time->modify(sprintf('today %s', $time_begin));
Peter Thienel
committed
$rec_date_end = $rec_date_begin->add($date->getDuration())->modify($time_end);
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
$rec_date->calendar_date->begin = $rec_date_begin->getTimestamp();
$rec_date->calendar_date->end = $rec_date_end->getTimestamp();
$index = $date->calendar_date->id . '_' . $rec_date_begin->getTimestamp();
return [$index => $rec_date];
}
//Event interface implementation:
public function getObjectId() : string
{
return (string)$this->id;
}
public function getPrimaryObjectID(): string
{
return $this->calendar_date_id;
}
public function getObjectClass(): string
{
return static::class;
}
public function getTitle() : string
{
return $this->calendar_date->title ?? '';
}
public function getBegin(): DateTime
{
$begin = new DateTime();
$begin->setTimestamp($this->calendar_date->begin ?? 0);
return $begin;
}
public function getEnd(): DateTime
{
$end = new DateTime();
$end->setTimestamp($this->calendar_date->end ?? 0);
return $end;
}
public function getDuration(): DateInterval
{
$begin = $this->getBegin();
$end = $this->getEnd();
return $begin->diff($end);
}
/**
* Returns the "extent" in days of this date.
*
* @return int The "extent" in days of this date.
*/
public function getDurationDays(): int
{
return self::getExtent($this->getEnd(), $this->getBegin());
}
/**
* Returns the "extent" in days of this date.
* The extent is the number of days a date is displayed in a calendar.
*
* @return int The "extent" in days of this date.
*/
public static function getExtent(DateTimeInterface $date_begin, DateTimeInterface $date_end): int
{
$days_duration = $date_end->diff($date_begin)->days;
if ($date_begin->format('His') > $date_end->format('His')) {
$days_duration += 1;
}
return $days_duration;
}
public function getLocation(): string
{
return $this->calendar_date->location ?? '';
}
public function getUniqueId(): string
{
return $this->calendar_date->unique_id ?? '';
}
public function getDescription(): string
{
return $this->calendar_date->description ?? '';
}
public function getAdditionalDescriptions(): array
{
return [
_('Kategorie') => $this->calendar_date->getCategoryAsString(),
_('Sichtbarkeit') => $this->calendar_date->getVisibilityAsString(),
_('Wiederholung') => $this->calendar_date->getRepetitionAsString()
];
}
public function isAllDayEvent(): bool
{
$begin = $this->getBegin();
Peter Thienel
committed
if ($begin->format('His') !== '000000') {
Peter Thienel
committed
$end = $this->getEnd();
Peter Thienel
committed
return $end->format('His') === '235959';
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
}
public function isWritable(string $user_id): bool
{
if ($this->calendar_date->author_id === $user_id) {
//The author may always modify one of their dates:
return true;
}
if ($this->calendar_date->isWritable($user_id)) {
//The date is writable.
return true;
}
//The user referenced by $user_id is not the author of the date.
//Check if they have write permissions to the calendar where the date is assigned to:
if ($this->user instanceof User) {
//It is a personal calendar. Check if the owner of the calendar has granted write permissions
//to the user:
return Contact::countBySQL(
"`owner_id` = :owner_id AND `user_id` = :user_id
AND `calendar_permissions` = 'WRITE'",
['owner_id' => $this->range_id, 'user_id' => $user_id]
) > 0;
} elseif ($this->course instanceof Course) {
//It is a course calendar.
return $GLOBALS['perm']->have_studip_perm('dozent', $this->range_id, $user_id);
}
//No write permissions are granted.
return false;
}
public function getCreationDate(): DateTime
{
$mkdate = new DateTime();
$mkdate->setTimestamp($this->calendar_date->mkdate ?? 0);
return $mkdate;
}
public function getModificationDate(): DateTime
{
$chdate = new DateTime();
$chdate->setTimestamp($this->calendar_date->chdate ?? 0);
return $chdate;
}
public function getImportDate(): DateTime
{
$import_date = new DateTime();
$import_date->setTimestamp($this->calendar_date->import_date ?? 0);
return $import_date;
}
public function getAuthor(): ?User
{
return $this->calendar_date->author ?? null;
}
public function getEditor(): ?User
{
return $this->calendar_date->editor ?? null;
}
/**
* TODO calculate end of repetition for different types of repetition
* @return float|int|object
*/
public function getExpire()
{
if ($this->calendar_date->repetition_end > 0) {
$expire = $this->calendar_date->repetition_end;
} else {
$expire = CalendarDate::NEVER_ENDING;
}
$end = new DateTime();
$end->setTimestamp($expire);
return $end;
}
// TODO calculate ts for monthly and yearly repetition
public function getNoonDate()
{
$ts = DateTimeImmutable::createFromMutable($this->getBegin());
switch ($this->calendar_date->repetition_type) {
case 'DAILY':
return $ts->modify('noon');
case 'WEEKLY':
return $ts->modify('monday this week noon');
case 'MONTHLY':
return $ts->modify('first day of this month noon');
case 'YEARLY':
return $ts->modify('first day of this year noon');
default:
return $ts;
}
}
/**
* Returns the type of repetition.
*
* @return string The type of repetition.
*/
public function getRepetitionType(): string
{
return $this->calendar_date->repetition_type;
}
public function toEventData(string $user_id): \Studip\Calendar\EventData
{
$begin = $this->getBegin();
$end = $this->getEnd();
Peter Thienel
committed
$all_day = $this->isAllDayEvent();
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
$hide_confidential_data = $this->calendar_date->access === 'CONFIDENTIAL'
&& $user_id !== $this->calendar_date->author_id;
$event_classes = ['user-date'];
$text_colour = '#000000';
$background_colour = '#ffffff';
$border_colour = '#000000';
if (!$hide_confidential_data) {
if ($this->calendar_date->user_category) {
//The date belongs to a personal category that gets a grey colour.
$background_colour = '#a7abaf';
$border_colour = '#a7abaf';
} else {
//The date belongs to a system category that has its own colours.
$text_colour = $GLOBALS['PERS_TERMIN_KAT'][$this->calendar_date->category]['fgcolor'] ?? $text_colour;
$background_colour = $GLOBALS['PERS_TERMIN_KAT'][$this->calendar_date->category]['bgcolor'] ?? $background_colour;
$border_colour = $GLOBALS['PERS_TERMIN_KAT'][$this->calendar_date->category]['border_color'] ?? $border_colour;
$event_classes[] = sprintf('user-date-category%d', $this->calendar_date->category);
}
}
$show_url_params = [];
if ($this->calendar_date->repetition_type) {
$show_url_params['selected_date'] = $begin->format('Y-m-d');
}
return new \Studip\Calendar\EventData(
$begin,
$end,
!$hide_confidential_data ? $this->getTitle() : '',
$event_classes,
$text_colour,
$background_colour,
$this->isWritable($user_id),
CalendarDateAssignment::class,
$this->id,
CalendarDate::class,
$this->calendar_date_id,
'user',
$this->range_id ?? '',
[
'show' => URLHelper::getURL('dispatch.php/calendar/date/index/' . $this->calendar_date_id, $show_url_params)
],
[
'resize_dialog' => URLHelper::getURL('dispatch.php/calendar/date/move/' . $this->calendar_date_id),
'move_dialog' => URLHelper::getURL('dispatch.php/calendar/date/move/' . $this->calendar_date_id)
],
$this->participation === 'DECLINED' ? 'decline-circle-full' : '',
$border_colour,
$all_day
);
}
public function getRangeName() : string
{
if ($this->course instanceof Course) {
return $this->course->getFullName();
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
} elseif ($this->user instanceof User) {
return $this->user->getFullName();
}
return '';
}
public function getRangeAvatar() : ?Avatar
{
if ($this->course instanceof Course) {
return CourseAvatar::getAvatar($this->range_id);
} elseif ($this->user instanceof User) {
return Avatar::getAvatar($this->range_id);
}
return null;
}
public function getParticipationAsString() : string
{
if ($this->participation === '') {
return _('Abwartend');
} elseif ($this->participation === 'ACKNOWLEDGED') {
return _('Angenommen (keine Teilnahme)');
} elseif ($this->participation === 'ACCEPTED') {
return _('Angenommen');
} elseif ($this->participation === 'DECLINED') {
return _('Abgelehnt');
}
return '';
}
}