diff options
author | dartcafe <github@dartcafe.de> | 2020-08-17 11:35:43 +0300 |
---|---|---|
committer | dartcafe <github@dartcafe.de> | 2020-08-17 11:35:43 +0300 |
commit | c679210764583b8a25df9c5e3a231eb0a8cfb9e3 (patch) | |
tree | 51227437e3f1e53798c90a5d43aca79cd4167b41 | |
parent | 56d48b45a12314db6dd424ad69a478e2e4c84cc9 (diff) |
find possible calender conflicts
-rw-r--r-- | appinfo/routes.php | 1 | ||||
-rw-r--r-- | lib/Controller/OptionController.php | 55 | ||||
-rw-r--r-- | lib/Service/CalendarService.php | 102 | ||||
-rw-r--r-- | lib/Service/OptionService.php | 16 | ||||
-rw-r--r-- | src/js/components/Base/CalendarInfo.vue | 103 | ||||
-rw-r--r-- | src/js/components/VoteTable/VoteTable.vue | 67 | ||||
-rw-r--r-- | src/js/components/VoteTable/VoteTableCalendarPeek.vue | 152 | ||||
-rw-r--r-- | src/js/components/VoteTable/VoteTableVoteItem.vue | 5 | ||||
-rw-r--r-- | src/js/store/modules/subModules/options.js | 13 |
9 files changed, 508 insertions, 6 deletions
diff --git a/appinfo/routes.php b/appinfo/routes.php index 1a3ff5c3..92efe489 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -47,6 +47,7 @@ return [ ['name' => 'option#confirm', 'url' => '/option/{optionId}/confirm', 'verb' => 'PUT'], ['name' => 'option#reorder', 'url' => '/options/reorder', 'verb' => 'POST'], ['name' => 'option#list', 'url' => '/polls/{pollId}/options', 'verb' => 'GET'], + ['name' => 'option#findCalendarEvents', 'url' => '/option/{optionId}/events', 'verb' => 'GET'], // ['name' => 'option#listByToken', 'url' => '/options/get/s/{token}', 'verb' => 'GET'], ['name' => 'vote#set', 'url' => '/vote/set', 'verb' => 'POST'], diff --git a/lib/Controller/OptionController.php b/lib/Controller/OptionController.php index 3b1e7f1e..e8fd8cad 100644 --- a/lib/Controller/OptionController.php +++ b/lib/Controller/OptionController.php @@ -23,6 +23,8 @@ namespace OCA\Polls\Controller; +use DateTime; +use DateInterval; use Exception; use OCP\IRequest; @@ -31,12 +33,17 @@ use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCA\Polls\Service\OptionService; +use OCA\Polls\Service\CalendarService; + class OptionController extends Controller { /** @var OptionService */ private $optionService; + /** @var CalendarService */ + private $calendarService; + /** * OptionController constructor. * @param string $appName @@ -47,10 +54,12 @@ class OptionController extends Controller { public function __construct( string $appName, IRequest $request, - OptionService $optionService + OptionService $optionService, + CalendarService $calendarService ) { parent::__construct($appName, $request); $this->optionService = $optionService; + $this->calendarService = $calendarService; } /** @@ -113,4 +122,48 @@ class OptionController extends Controller { public function reorder($pollId, $options) { return new DataResponse(['options' => $this->optionService->reorder($pollId, $options)], Http::STATUS_OK); } + + /** + * findCalendarEvents + * @NoAdminRequired + * @param integer $from + * @param integer $to + * @return DataResponse + */ + public function findCalendarEvents($optionId) { + + $searchFrom = new DateTime(); + $searchFrom = $searchFrom->setTimestamp($this->optionService->get($optionId)->getTimestamp())->sub(new DateInterval('PT1H')); + $searchTo = clone $searchFrom; + $searchTo = $searchTo->add(new DateInterval('PT3H')); + + \OC::$server->getLogger()->alert('Search events between ' . $searchFrom->format('Y-m-d H:i:s') . ' and ' . $searchTo->format('Y-m-d H:i:s')); + + return new DataResponse(['events' => array_values($this->calendarService->getEvents($searchFrom, $searchTo))], Http::STATUS_OK); + + + if (is_int($from)) { + $searchFrom = new DateTime(); + $searchFrom = $searchFrom->setTimestamp($from); + } else { + $searchFrom = new DateTime($from); + } + + + if (!$to) { + $searchTo = clone $searchFrom; + $searchTo = $searchTo->add(new DateInterval('PT1H')); + + } else if (is_int($to)) { + $searchTo = new DateTime(); + $searchTo = $searchTo->setTimestamp($to); + } else { + $searchTo = new DateTime($to); + } + + $events = array_values($this->calendarService->getEvents($searchFrom, $searchTo)); + return $events; + + } + } diff --git a/lib/Service/CalendarService.php b/lib/Service/CalendarService.php new file mode 100644 index 00000000..68f9c0f2 --- /dev/null +++ b/lib/Service/CalendarService.php @@ -0,0 +1,102 @@ +<?php +/** + * @copyright Copyright (c) 2017 Vinzenz Rosenkranz <vinzenz.rosenkranz@gmail.com> + * + * @author René Gieling <github@dartcafe.de> +* + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + + +namespace OCA\Polls\Service; + +use DateTime; +use OCP\Calendar\IManager as CalendarManager; +use OCP\Calendar\ICalendar; + +class CalendarService { + + private $calendarManager; + private $calendars; + + public function __construct( + CalendarManager $calendarManager + ) { + $this->calendarManager = $calendarManager; + $this->calendars = $this->calendarManager->getCalendars(); + } + + /** + * getEvents - get events from the user's calendars inside given timespan + * @NoAdminRequired + * @param DateTime $from + * @param DateTime $to + * @return Array + */ + public function getEvents($from, $to) { + $events = []; + + foreach ($this->calendars as $calendar) { + $foundEvents = $calendar->search('' ,['SUMMARY'], ['timerange' => ['start' => $from, 'end' => $to]]); + foreach ($foundEvents as $event) { + array_push($events, [ + 'relatedFrom' => $from->getTimestamp(), + 'relatedTo' => $to->getTimestamp(), + 'name' => $calendar->getDisplayName(), + 'key' => $calendar->getKey(), + 'displayColor' => $calendar->getDisplayColor(), + 'permissions' => $calendar->getPermissions(), + 'eventId' => $event['id'], + 'UID' => $event['objects'][0]['UID'][0], + 'summary' => isset($event['objects'][0]['SUMMARY'][0])? $event['objects'][0]['SUMMARY'][0] : '', + 'description' => isset($event['objects'][0]['DESCRIPTION'][0])? $event['objects'][0]['DESCRIPTION'][0] : '', + 'location' => isset($event['objects'][0]['LOCATION'][0]) ? $event['objects'][0]['LOCATION'][0] : '', + 'eventFrom' => isset($event['objects'][0]['DTSTART'][0]) ? $event['objects'][0]['DTSTART'][0]->getTimestamp() : 0, + 'eventTo' => isset($event['objects'][0]['DTEND'][0] ) ? $event['objects'][0]['DTEND'][0]->getTimestamp() : 0, + 'calDav' => $event + ]); + } + } + return $events; + } + + /** + * Get user's calendars + * @NoAdminRequired + * @return Array + */ + public function getCalendars() { + return $this->calendars; + } + + + /** + * Get events from the user's calendar which are 2 hours before and 3 hours after the timestamp + * @NoAdminRequired + * @param DateTime $from + * @return Array + */ + public function getEventsAround($from) { + $from = new DateTime($from); + $to = new DateTime($from); + return $this->getEvents( + $from->sub(new DateInterval('P2H')), + $to->add(new DateInterval('P3H')) + ); + } + +} diff --git a/lib/Service/OptionService.php b/lib/Service/OptionService.php index 7208a154..0e2dbb88 100644 --- a/lib/Service/OptionService.php +++ b/lib/Service/OptionService.php @@ -103,6 +103,22 @@ class OptionService { } } + /** + * Get option + * @NoAdminRequired + * @param int $optionId + * @return Option + * @throws NotAuthorizedException + */ + public function get($optionId) { + + if (!$this->acl->set($this->optionMapper->find($optionId)->getPollId())->getAllowView()) { + throw new NotAuthorizedException; + } + + return $this->optionMapper->find($optionId); + } + /** * Add a new option diff --git a/src/js/components/Base/CalendarInfo.vue b/src/js/components/Base/CalendarInfo.vue new file mode 100644 index 00000000..750dabd2 --- /dev/null +++ b/src/js/components/Base/CalendarInfo.vue @@ -0,0 +1,103 @@ +<!-- + - @copyright Copyright (c) 2018 René Gieling <github@dartcafe.de> + - + - @author René Gieling <github@dartcafe.de> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <div class="calendar-info" + :class="conflictLevel(event)" + :style="{ backgroundColor: event.displayColor }"> + <div class="calendar-info__time"> + {{ formatDate(event.eventFrom) }} - {{ formatDate(event.eventTo) }} + </div> + <div class="calendar-info__summay"> + {{ event.summary }} + </div> + </div> +</template> + +<script> + +import moment from '@nextcloud/moment' +export default { + name: 'CalendarInfo', + + props: { + event: { + type: Object, + default: undefined, + }, + + option: { + type: Object, + default: undefined, + }, + + }, + + methods: { + formatDate(timeStamp) { + return moment.unix(timeStamp).format('LT') + }, + conflictLevel(event) { + if (event.key === 0) { + return 'conflict-ignore' + } else if (event.eventFrom >= this.option.timestamp + 3600) { + return 'conflict-no' + } else if (event.eventTo <= this.option.timestamp) { + return 'conflict-no' + } else { + return 'conflict-yes' + } + }, + }, +} + +</script> + +<style lang="scss"> + +.calendar-info { + display: flex; + align-items: center; + border-radius: var(--border-radius); + margin: 4px 0; + padding: 0 4px; + + &.conflict-ignore { + border-left: 4px solid transparent; + } + + &.conflict-no { + border-left: 4px solid var(--color-success); + } + + &.conflict-yes { + border-left: 4px solid var(--color-error); + } +} + +.calendar-info__time { + width: 65px; + font-size: 80%; + flex: 0 auto; +} + +</style> diff --git a/src/js/components/VoteTable/VoteTable.vue b/src/js/components/VoteTable/VoteTable.vue index f6063976..2e458f03 100644 --- a/src/js/components/VoteTable/VoteTable.vue +++ b/src/js/components/VoteTable/VoteTable.vue @@ -43,6 +43,14 @@ :table-mode="tableMode" /> </div> + <div v-if="poll.type === 'datePoll'" class="vote-table__calendar"> + <VoteTableCalendarPeek + v-for="(option) in rankedOptions" + :key="option.id" + :option="option" + :open="false" /> + </div> + <div class="vote-table__votes"> <div v-for="(participant) in participants" :key="participant.userId" @@ -70,6 +78,8 @@ <div class="vote-table__footer-blind fixed" /> + <div class="vote-table__calendar-blind fixed" /> + <div class="vote-table__header-blind fixed" /> <Modal v-if="modal"> @@ -90,6 +100,7 @@ import { mapState, mapGetters } from 'vuex' import { Actions, ActionButton, Modal } from '@nextcloud/vue' import orderBy from 'lodash/orderBy' +import VoteTableCalendarPeek from './VoteTableCalendarPeek' import VoteTableVoteItem from './VoteTableVoteItem' import VoteTableHeaderItem from './VoteTableHeaderItem' import { confirmOption } from '../../mixins/optionMixins' @@ -100,6 +111,7 @@ export default { Actions, ActionButton, Modal, + VoteTableCalendarPeek, VoteTableHeaderItem, VoteTableVoteItem, }, @@ -168,6 +180,7 @@ export default { // define default flex items .vote-table__users, .vote-table__header, + .vote-table__calendar, .vote-table__votes, .vote-table__footer, .vote-table__vote-row, @@ -178,6 +191,7 @@ export default { //set default style for confirmed options .vote-table__header, + .vote-table__calendar, .vote-table__vote-row, .vote-table__footer { > div { @@ -200,7 +214,7 @@ export default { .vote-table.mobile { grid-template-columns: auto 1fr; grid-template-rows: auto; - grid-template-areas: 'vote header'; + grid-template-areas: 'vote calendar header'; justify-items: stretch; .vote-table__header { @@ -221,6 +235,23 @@ export default { } } + .vote-table__calendar { + grid-area: calendar; + flex-direction: column; + + > div { + flex-direction: row; + flex: 1; + align-items: center; + + &.confirmed { + border-bottom: none !important; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + } + } + .vote-table__header-blind, .vote-table__users, .vote-table__vote-row:not(.currentuser), @@ -230,6 +261,7 @@ export default { } .vote-table__header, + .vote-table__calendar, .vote-table__vote-row { > div { padding-left: 12px; @@ -245,7 +277,8 @@ export default { } } - .vote-table__vote-row { + .vote-table__vote-row, + .vote-table__calendar { > div.confirmed { border-right: none !important; border-top-right-radius: 0; @@ -260,8 +293,9 @@ export default { grid-template-rows: auto repeat(var(--polls-vote-rows), 1fr) auto; grid-template-areas: 'blind1 options' + 'blind2 calendar' 'users vote' - 'blind2 footer'; + 'blind3 footer'; justify-items: stretch; padding-bottom: 14px; // leave space for the scrollbar! @@ -282,14 +316,35 @@ export default { } } + .vote-table__calendar { + grid-area: calendar; + flex-direction: row; + + > div { + flex-direction: column; + flex: 1; + align-items: center; + + &.confirmed { + border-bottom: none !important; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + } + } + .vote-table__header-blind { grid-area: blind1; } - .vote-table__footer-blind { + .vote-table__calendar-blind { grid-area: blind2; } + .vote-table__footer-blind { + grid-area: blind3; + } + .vote-table__votes { grid-area: vote; flex-direction: column; @@ -325,6 +380,7 @@ export default { .vote-table__header, .vote-table__vote-row, + .vote-table__calendar, .vote-table__footer { > div { max-width: 230px; @@ -338,7 +394,8 @@ export default { } // limit width of columns - .vote-table__vote-row { + .vote-table__vote-row, + .vote-table__calendar { flex-direction: row; order: 1; flex: 1; diff --git a/src/js/components/VoteTable/VoteTableCalendarPeek.vue b/src/js/components/VoteTable/VoteTableCalendarPeek.vue new file mode 100644 index 00000000..bc135015 --- /dev/null +++ b/src/js/components/VoteTable/VoteTableCalendarPeek.vue @@ -0,0 +1,152 @@ +<!-- + - @copyright Copyright (c) 2018 René Gieling <github@dartcafe.de> + - + - @author René Gieling <github@dartcafe.de> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <div class="vote-table-calendar-peek"> + <Popover> + <div v-if="events.length" slot="trigger"> + <div class="conflict icon icon-calendar" /> + <p>{{ t('polls', 'Conflict') }}</p> + </div> + <div class="calendar-grid"> + <CalendarInfo v-for="eventItem in sortedEvents" :key="eventItem.UID" + :event="eventItem" + :option="option" /> + </div> + </Popover> + </div> +</template> + +<script> + +import { mapState } from 'vuex' +import orderBy from 'lodash/orderBy' +import { Popover } from '@nextcloud/vue' +import CalendarInfo from '../Base/CalendarInfo' + +export default { + name: 'VoteTableCalendarPeek', + + components: { + CalendarInfo, + Popover, + }, + + props: { + option: { + type: Object, + default: undefined, + }, + open: { + type: Boolean, + default: false, + }, + }, + + data() { + return { + events: [], + event: { + relatedFrom: 0, + relatedTo: 0, + name: '', + key: '', + displayColor: '', + permissions: 0, + eventId: 0, + UID: 0, + summary: '', + description: '', + location: '', + eventFrom: '', + eventTo: '', + }, + } + }, + + computed: { + ...mapState({ + poll: state => state.poll, + }), + + sortedEvents() { + var sortedEvents = [...this.events] + sortedEvents.push(this.thisOption) + return orderBy(sortedEvents, ['eventFrom', 'eventTo'], ['asc', 'asc']) + }, + + thisOption() { + return { + name: 'Polls', + key: 0, + displayColor: '#fff', + permissions: 0, + eventId: this.option.id, + UID: this.option.id, + summary: this.poll.title, + description: this.poll.description, + location: '', + eventFrom: this.option.timestamp, + eventTo: this.option.timestamp + 3600, + } + }, + }, + + mounted() { + this.getEvents() + }, + + methods: { + getEvents() { + this.$store + .dispatch('poll/options/getEvents', { option: this.option }) + .then((response) => { + this.events = response.events + }) + }, + + }, +} + +</script> + +<style lang="scss"> + +.conflict.icon { + font-style: normal; + font-weight: 400; + width: 32px; + height: 32px; + // background-size: 28px; + background-color: var(--color-warning); + border-radius: 50%; + margin: 4px auto; +} + +.vote-table-calendar-peek >div { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +</style> diff --git a/src/js/components/VoteTable/VoteTableVoteItem.vue b/src/js/components/VoteTable/VoteTableVoteItem.vue index 7a87a5b7..62e2af7f 100644 --- a/src/js/components/VoteTable/VoteTableVoteItem.vue +++ b/src/js/components/VoteTable/VoteTableVoteItem.vue @@ -84,6 +84,11 @@ export default { }, methods: { + getEvents() { + this.$store + .dispatch('poll/options/getEvents', { option: this.option }) + }, + setVote() { this.$store .dispatch('poll/votes/set', { diff --git a/src/js/store/modules/subModules/options.js b/src/js/store/modules/subModules/options.js index e2536710..02f8fa5c 100644 --- a/src/js/store/modules/subModules/options.js +++ b/src/js/store/modules/subModules/options.js @@ -198,6 +198,19 @@ const actions = { throw error }) }, + + getEvents(context, payload) { + const endPoint = 'apps/polls/option' + return axios.get(generateUrl(endPoint.concat('/', payload.option.id, '/events'))) + .then((response) => { + return response.data + }) + .catch((error) => { + console.error('Error reordering option', { error: error.response }, { payload: payload }) + context.dispatch('reload') + throw error + }) + }, } export default { state, mutations, getters, actions, namespaced } |