Passed
Pull Request — master (#1449)
by René
06:19 queued 01:36
created

OptionService::moveModifier()   A

Complexity

Conditions 6
Paths 4

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 8
c 1
b 0
f 0
dl 0
loc 12
ccs 0
cts 11
cp 0
rs 9.2222
cc 6
nc 4
nop 3
crap 42
1
<?php
2
/**
3
 * @copyright Copyright (c) 2017 Vinzenz Rosenkranz <[email protected]>
4
 *
5
 * @author René Gieling <[email protected]>
6
 *
7
 * @license GNU AGPL version 3 or any later version
8
 *
9
 *  This program is free software: you can redistribute it and/or modify
10
 *  it under the terms of the GNU Affero General Public License as
11
 *  published by the Free Software Foundation, either version 3 of the
12
 *  License, or (at your option) any later version.
13
 *
14
 *  This program is distributed in the hope that it will be useful,
15
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
 *  GNU Affero General Public License for more details.
18
 *
19
 *  You should have received a copy of the GNU Affero General Public License
20
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
21
 *
22
 */
23
24
namespace OCA\Polls\Service;
25
26
use DateTime;
27
use OCP\AppFramework\Db\DoesNotExistException;
28
use OCA\Polls\Exceptions\NotAuthorizedException;
29
use OCA\Polls\Exceptions\BadRequestException;
30
use OCA\Polls\Exceptions\DuplicateEntryException;
31
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
32
33
use OCA\Polls\Db\OptionMapper;
34
use OCA\Polls\Db\VoteMapper;
35
use OCA\Polls\Db\Vote;
36
use OCA\Polls\Db\Option;
37
use OCA\Polls\Db\PollMapper;
38
use OCA\Polls\Db\Poll;
39
use OCA\Polls\Db\Watch;
40
use OCA\Polls\Model\Acl;
41
42
class OptionService {
43
44
	/** @var Acl */
45
	private $acl;
46
47
	/** @var Option */
48
	private $option;
49
50
	/** @var int */
51
	private $countParticipants;
52
53
	/** @var Poll */
54
	private $poll;
55
56
	/** @var Option[] */
57
	private $options;
58
59
	/** @var Vote[] */
60
	private $votes;
61
62
	/** @var OptionMapper */
63
	private $optionMapper;
64
65
	/** @var PollMapper */
66
	private $pollMapper;
67
68
	/** @var VoteMapper */
69
	private $voteMapper;
70
71
	/** @var WatchService */
72
	private $watchService;
73
74
	public function __construct(
75
		Acl $acl,
76
		Option $option,
77
		OptionMapper $optionMapper,
78
		PollMapper $pollMapper,
79
		VoteMapper $voteMapper,
80
		WatchService $watchService
81
	) {
82
		$this->acl = $acl;
83
		$this->option = $option;
84
		$this->optionMapper = $optionMapper;
85
		$this->pollMapper = $pollMapper;
86
		$this->voteMapper = $voteMapper;
87
		$this->watchService = $watchService;
88
	}
89
90
	/**
91
	 * 	 * Get all options of given poll
92
	 *
93
	 * @return Option[]
94
	 *
95
	 * @psalm-return array<array-key, Option>
96
	 */
97
	public function list(int $pollId = 0, string $token = ''): array {
98
		if ($token) {
99
			$this->acl->setToken($token);
100
			$pollId = $this->acl->getPollId();
101
		} else {
102
			$this->acl->setPollId($pollId)->request(Acl::PERMISSION_VIEW);
103
		}
104
105
		if (!$this->acl->isAllowed(Acl::PERMISSION_VIEW)) {
106
			throw new NotAuthorizedException;
107
		}
108
109
		try {
110
			$this->poll = $this->pollMapper->find($pollId);
111
			$this->options = $this->optionMapper->findByPoll($pollId);
112
			$this->votes = $this->voteMapper->findByPoll($pollId);
113
			$this->countParticipants = count($this->voteMapper->findParticipantsByPoll($pollId));
114
115
			$this->calculateVotes();
116
117
			if ($this->poll->getHideBookedUp() && !$this->acl->isAllowed(Acl::PERMISSION_EDIT)) {
118
				// hide booked up options except the user has edit permission
119
				$this->filterBookedUp();
120
			} elseif ($this->acl->isAllowed(Acl::PERMISSION_SEE_RESULTS)) {
121
				$this->calculateRanks();
122
			}
123
124
			return array_values($this->options);
125
		} catch (DoesNotExistException $e) {
126
			return [];
127
		}
128
	}
129
130
	/**
131
	 * 	 * Get option
132
	 *
133
	 * @return Option
134
	 */
135
	public function get(int $optionId): Option {
136
		$this->acl->setPollId($this->optionMapper->find($optionId)->getPollId())->request(Acl::PERMISSION_VIEW);
137
138
		if (!$this->acl->isAllowed(Acl::PERMISSION_VIEW)) {
139
			throw new NotAuthorizedException;
140
		}
141
142
		return $this->optionMapper->find($optionId);
143
	}
144
145
146
	/**
147
	 * 	 * Add a new option
148
	 *
149
	 * @return Option
150
	 */
151
	public function add(int $pollId, int $timestamp = 0, string $pollOptionText = '', ?int $duration = 0): Option {
152
		$this->acl->setPollId($pollId)->request(Acl::PERMISSION_EDIT);
153
		$this->option = new Option();
154
		$this->option->setPollId($pollId);
155
		$this->option->setOrder($this->getHighestOrder($this->option->getPollId()) + 1);
156
		$this->setOption($timestamp, $pollOptionText, $duration);
157
158
		try {
159
			$this->option = $this->optionMapper->insert($this->option);
160
			$this->watchService->writeUpdate($this->option->getPollId(), Watch::OBJECT_OPTIONS);
161
		} catch (UniqueConstraintViolationException $e) {
162
			throw new DuplicateEntryException('This option already exists');
163
		}
164
		return $this->option;
165
	}
166
167
	/**
168
	 * 	 * Update option
169
	 *
170
	 * @return Option
171
	 */
172
	public function update(int $optionId, int $timestamp = 0, ?string $pollOptionText = '', ?int $duration = 0): Option {
173
		$this->option = $this->optionMapper->find($optionId);
174
		$this->acl->setPollId($this->option->getPollId())->request(Acl::PERMISSION_EDIT);
175
		$this->setOption($timestamp, $pollOptionText, $duration);
176
177
		$this->option = $this->optionMapper->update($this->option);
178
		$this->watchService->writeUpdate($this->option->getPollId(), Watch::OBJECT_OPTIONS);
179
		return $this->option;
180
	}
181
182
	/**
183
	 * 	 * Delete option
184
	 *
185
	 * @return Option
186
	 */
187
	public function delete(int $optionId): Option {
188
		$this->option = $this->optionMapper->find($optionId);
189
		$this->acl->setPollId($this->option->getPollId())->request(Acl::PERMISSION_EDIT);
190
		$this->optionMapper->delete($this->option);
191
		$this->watchService->writeUpdate($this->option->getPollId(), Watch::OBJECT_OPTIONS);
192
193
		return $this->option;
194
	}
195
196
	/**
197
	 * 	 * Switch optoin confirmation
198
	 *
199
	 * @return Option
200
	 */
201
	public function confirm(int $optionId): Option {
202
		$this->option = $this->optionMapper->find($optionId);
203
		$this->acl->setPollId($this->option->getPollId())->request(Acl::PERMISSION_EDIT);
204
205
		$this->option->setConfirmed($this->option->getConfirmed() ? 0 : time());
206
		$this->option = $this->optionMapper->update($this->option);
207
		$this->watchService->writeUpdate($this->option->getPollId(), Watch::OBJECT_OPTIONS);
208
		return $this->option;
209
	}
210
211
	/**
212
	 * 	 * Make a sequence of date poll options
213
	 * @param int $optionId
214
	 * @param int $step - The step for creating the sequence
215
	 * @param string $unit - The timeunit (year, month, ...)
216
	 * @param int $amount - Number of sequence items to create
217
	 *
218
	 * @return Option[]
219
	 *
220
	 * @psalm-return array<array-key, Option>
221
	 */
222
	public function sequence(int $optionId, int $step, string $unit, int $amount): array {
223
		$baseDate = new DateTime;
224
		$this->option = $this->optionMapper->find($optionId);
225
		$this->acl->setPollId($this->option->getPollId())->request(Acl::PERMISSION_EDIT);
226
227
		if ($step === 0) {
228
			return $this->optionMapper->findByPoll($this->option->getPollId());
229
		}
230
231
		$baseDate->setTimestamp($this->option->getTimestamp());
232
233
		for ($i = 0; $i < $amount; $i++) {
234
			$clonedOption = new Option();
235
			$clonedOption->setPollId($this->option->getPollId());
236
			$clonedOption->setDuration($this->option->getDuration());
237
			$clonedOption->setConfirmed(0);
238
			$clonedOption->setTimestamp($baseDate->modify($step . ' ' . $unit)->getTimestamp());
239
			$clonedOption->setOrder($clonedOption->getTimestamp());
240
			$clonedOption->setPollOptionText($baseDate->format('c'));
241
			try {
242
				$this->optionMapper->insert($clonedOption);
243
			} catch (UniqueConstraintViolationException $e) {
244
				\OC::$server->getLogger()->warning('skip adding ' . $baseDate->format('c') . 'for pollId' . $this->option->getPollId() . '. Option alredy exists.');
245
			}
246
		}
247
		$this->watchService->writeUpdate($this->option->getPollId(), Watch::OBJECT_OPTIONS);
248
		return $this->optionMapper->findByPoll($this->option->getPollId());
249
	}
250
251
	/**
252
	 * 	 * Copy options from $fromPoll to $toPoll
253
	 *
254
	 * @return Option[]
255
	 *
256
	 * @psalm-return array<array-key, Option>
257
	 */
258
	public function clone(int $fromPollId, int $toPollId): array {
259
		$this->acl->setPollId($fromPollId);
260
261
		foreach ($this->optionMapper->findByPoll($fromPollId) as $origin) {
262
			$option = new Option();
263
			$option->setPollId($toPollId);
264
			$option->setConfirmed(0);
265
			$option->setPollOptionText($origin->getPollOptionText());
266
			$option->setTimestamp($origin->getTimestamp());
267
			$option->setDuration($origin->getDuration());
268
			$option->setOrder($option->getOrder());
269
			$this->optionMapper->insert($option);
270
		}
271
272
		return $this->optionMapper->findByPoll($toPollId);
273
	}
274
275
	/**
276
	 * Reorder options with the order specified by $options
277
	 *
278
	 * @return Option[]
279
	 *
280
	 * @psalm-return array<array-key, Option>
281
	 */
282
	public function reorder(int $pollId, array $options): array {
283
		try {
284
			$this->poll = $this->pollMapper->find($pollId);
285
			$this->acl->setPoll($this->poll)->request(Acl::PERMISSION_EDIT);
286
287
			if ($this->poll->getType() === Poll::TYPE_DATE) {
288
				throw new BadRequestException("Not allowed in date polls");
289
			}
290
		} catch (DoesNotExistException $e) {
291
			throw new NotAuthorizedException;
292
		}
293
294
		$i = 0;
295
		foreach ($options as $option) {
296
			$this->option = $this->optionMapper->find($option['id']);
297
			if ($pollId === intval($this->option->getPollId())) {
298
				$this->option->setOrder(++$i);
299
				$this->optionMapper->update($this->option);
300
			}
301
		}
302
303
		$this->watchService->writeUpdate($pollId, Watch::OBJECT_OPTIONS);
304
		return $this->optionMapper->findByPoll($pollId);
305
	}
306
307
	/**
308
	 * Change order for $optionId and reorder the options
309
	 *
310
	 * @return Option[]
311
	 *
312
	 * @psalm-return array<array-key, Option>
313
	 */
314
	public function setOrder(int $optionId, int $newOrder): array {
315
		try {
316
			$this->option = $this->optionMapper->find($optionId);
317
			$this->poll = $this->pollMapper->find($this->option->getPollId());
318
			$this->acl->setPoll($this->poll)->request(Acl::PERMISSION_EDIT);
319
320
			if ($this->poll->getType() === Poll::TYPE_DATE) {
321
				throw new BadRequestException("Not allowed in date polls");
322
			}
323
		} catch (DoesNotExistException $e) {
324
			throw new NotAuthorizedException;
325
		}
326
327
		if ($newOrder < 1) {
328
			$newOrder = 1;
329
		} elseif ($newOrder > $this->getHighestOrder($this->poll->getId())) {
330
			$newOrder = $this->getHighestOrder($this->poll->getId());
331
		}
332
333
		foreach ($this->optionMapper->findByPoll($this->poll->getId()) as $option) {
334
			$option->setOrder($this->moveModifier($this->option->getOrder(), $newOrder, $option->getOrder()));
335
			$this->optionMapper->update($option);
336
		}
337
338
		$this->watchService->writeUpdate($this->option->getPollId(), Watch::OBJECT_OPTIONS);
339
		return $this->optionMapper->findByPoll($this->option->getPollId());
340
	}
341
342
	/**
343
	 * moveModifier - evaluate new order depending on the old and
344
	 * the new position of a moved array item
345
	 * @param int $moveFrom - old position of the moved item
346
	 * @param int $moveTo - target posotion of the moved item
347
	 * @param int $currentPosition - current position of the current item
348
	 *
349
	 * @return int - The modified new new position of the current item
350
	 */
351
	private function moveModifier(int $moveFrom, int $moveTo, int $currentPosition): int {
352
		$moveModifier = 0;
353
		if ($moveFrom < $currentPosition && $currentPosition <= $moveTo) {
354
			// moving forward
355
			$moveModifier = -1;
356
		} elseif ($moveTo <= $currentPosition && $currentPosition < $moveFrom) {
357
			//moving backwards
358
			$moveModifier = 1;
359
		} elseif ($moveFrom === $currentPosition) {
360
			return $moveTo;
361
		}
362
		return $currentPosition + $moveModifier;
363
	}
364
365
	/**
366
	 * Set option entities validated
367
	 *
368
	 * @return void
369
	 */
370
	private function setOption(int $timestamp = 0, ?string $pollOptionText = '', ?int $duration = 0): void {
371
		$this->poll = $this->pollMapper->find($this->option->getPollId());
372
373
		if ($this->poll->getType() === Poll::TYPE_DATE) {
374
			$this->option->setTimestamp($timestamp);
375
			$this->option->setOrder($timestamp);
376
			$this->option->setDuration($duration);
377
			if ($duration === 0) {
378
				$this->option->setPollOptionText(date('c', $timestamp));
379
			} elseif ($duration > 0) {
380
				$this->option->setPollOptionText(date('c', $timestamp) .' - ' . date('c', $timestamp + $duration));
381
			} else {
382
				$this->option->setPollOptionText($pollOptionText);
383
			}
384
		} else {
385
			$this->option->setPollOptionText($pollOptionText);
386
		}
387
	}
388
389
	/**
390
	 * Get all voteOptionTexts of the options, the user opted in
391
	 * with yes or maybe
392
	 *
393
	 * @return array
394
	 */
395
	private function getUsersVotes() {
396
		// Thats an ugly solution, but for now, it seems to work
397
		// Optimization proposals are welcome
398
399
		// First: Find votes, where the user voted yes or maybe
400
		$userId = $this->acl->getUserId();
401
		$exceptVotes = array_filter($this->votes, function ($vote) use ($userId) {
402
			if ($vote->getUserId() === $userId && in_array($vote->getVoteAnswer(), ['yes', 'maybe'])) {
403
				return $vote;
404
			}
405
		});
406
407
		// Second: Extract only the vote option texts to an array
408
		return array_values(array_map(function ($vote) {
409
			return $vote->getVoteOptionText();
410
		}, $exceptVotes));
411
	}
412
413
	/**
414
	 * Remove booked up options, because they are not votable
415
	 *
416
	 * @return void
417
	 */
418
	private function filterBookedUp() {
419
		$exceptVotes = $this->getUsersVotes();
420
		$this->options = array_filter($this->options, function ($option) use ($exceptVotes) {
421
			if (!$option->getIsBookedUp() || in_array($option->getPollOptionText(), $exceptVotes)) {
422
				return $option;
423
			}
424
		});
425
	}
426
427
	/**
428
	 * Calculate the votes of each option
429
	 * unvoted counts as no
430
	 * realno reports the actually opted out votes
431
	 *
432
	 * @return void
433
	 */
434
	private function calculateVotes() {
435
		foreach ($this->options as $option) {
436
			$option->yes = count(
437
				array_filter($this->votes, function ($vote) use ($option) {
438
					if ($vote->getVoteOptionText() === $option->getPollOptionText()
439
						&& $vote->getVoteAnswer() === 'yes') {
440
						return $vote;
441
					}
442
				})
443
			);
444
445
			$option->realno = count(
446
				array_filter($this->votes, function ($vote) use ($option) {
447
					if ($vote->getVoteOptionText() === $option->getPollOptionText()
448
						&& $vote->getVoteAnswer() === 'no') {
449
						return $vote;
450
					}
451
				})
452
			);
453
454
			$option->maybe = count(
455
				array_filter($this->votes, function ($vote) use ($option) {
456
					if ($vote->getVoteOptionText() === $option->getPollOptionText()
457
						&& $vote->getVoteAnswer() === 'maybe') {
458
						return $vote;
459
					}
460
				})
461
			);
462
463
			$option->isBookedUp = $this->poll->getOptionLimit() ? $this->poll->getOptionLimit() <= $option->yes : false;
464
465
			if (!$this->acl->isAllowed(Acl::PERMISSION_SEE_RESULTS)) {
466
				$option->yes = 0;
467
				$option->no = 0;
468
				$option->maybe = 0;
469
				$option->realNo = 0;
0 ignored issues
show
Bug introduced by
The property realNo does not exist on OCA\Polls\Db\Option. Did you mean realno?
Loading history...
470
			} else {
471
				$option->no = $this->countParticipants - $option->maybe - $option->yes;
472
			}
473
		}
474
	}
475
476
	/**
477
	 * Calculate the rank of each option based on the
478
	 * yes and maybe votes and recognize equal ranked options
479
	 *
480
	 * @return void
481
	 */
482
	private function calculateRanks() {
483
		// sort array by yes and maybe votes
484
		usort($this->options, function ($a, $b) {
485
			$diff = $b->yes - $a->yes;
486
			return ($diff !== 0) ? $diff : $b->maybe - $a->maybe;
487
		});
488
489
		// calculate the rank
490
		$count = count($this->options);
491
		for ($i = 0; $i < $count; $i++) {
492
			if ($i > 0 && $this->options[$i]->yes === $this->options[$i - 1]->yes && $this->options[$i]->maybe === $this->options[$i - 1]->maybe) {
493
				$this->options[$i]->rank = $this->options[$i - 1]->rank;
494
			} else {
495
				$this->options[$i]->rank = $i + 1;
496
			}
497
		}
498
499
		// restore original order
500
		usort($this->options, function ($a, $b) {
501
			return $a->getOrder() - $b->getOrder();
502
		});
503
	}
504
505
	/**
506
	 * 	 * Get the highest order number in $pollId
507
	 * 	 * Return Highest order number
508
	 *
509
	 * @return int
510
	 */
511
	private function getHighestOrder(int $pollId): int {
512
		$highestOrder = 0;
513
		foreach ($this->optionMapper->findByPoll($pollId) as $option) {
514
			$highestOrder = ($option->getOrder() > $highestOrder) ? $option->getOrder() : $highestOrder;
515
		}
516
		return $highestOrder;
517
	}
518
}
519