Passed
Pull Request — master (#1449)
by René
04:26
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 0
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\Option;
36
use OCA\Polls\Db\PollMapper;
37
use OCA\Polls\Db\Poll;
38
use OCA\Polls\Db\Watch;
39
use OCA\Polls\Model\Acl;
40
41
class OptionService {
42
43
	/** @var Acl */
44
	private $acl;
45
46
	/** @var Option */
47
	private $option;
48
49
	/** @var OptionMapper */
50
	private $optionMapper;
51
52
	/** @var PollMapper */
53
	private $pollMapper;
54
55
	/** @var VoteMapper */
56
	private $voteMapper;
57
58
	/** @var WatchService */
59
	private $watchService;
60
61
	public function __construct(
62
		Acl $acl,
63
		Option $option,
64
		OptionMapper $optionMapper,
65
		PollMapper $pollMapper,
66
		VoteMapper $voteMapper,
67
		WatchService $watchService
68
	) {
69
		$this->acl = $acl;
70
		$this->option = $option;
71
		$this->optionMapper = $optionMapper;
72
		$this->pollMapper = $pollMapper;
73
		$this->voteMapper = $voteMapper;
74
		$this->watchService = $watchService;
75
	}
76
77
	/**
78
	 * 	 * Get all options of given poll
79
	 *
80
	 * @return Option[]
81
	 *
82
	 * @psalm-return array<array-key, Option>
83
	 */
84
	public function list(?int $pollId = 0, string $token = ''): array {
85
		if ($token) {
86
			$this->acl->setToken($token);
87
			$pollId = $this->acl->getPollId();
88
		} else {
89
			$this->acl->setPollId($pollId)->request(Acl::PERMISSION_VIEW);
90
		}
91
92
		if (!$this->acl->isAllowed(Acl::PERMISSION_VIEW)) {
93
			throw new NotAuthorizedException;
94
		}
95
96
		try {
97
			$poll = $this->pollMapper->find($pollId);
0 ignored issues
show
Bug introduced by
It seems like $pollId can also be of type null; however, parameter $id of OCA\Polls\Db\PollMapper::find() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

97
			$poll = $this->pollMapper->find(/** @scrutinizer ignore-type */ $pollId);
Loading history...
98
			$options = $this->optionMapper->findByPoll($pollId);
0 ignored issues
show
Bug introduced by
It seems like $pollId can also be of type null; however, parameter $pollId of OCA\Polls\Db\OptionMapper::findByPoll() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

98
			$options = $this->optionMapper->findByPoll(/** @scrutinizer ignore-type */ $pollId);
Loading history...
99
			$votes = $this->voteMapper->findByPoll($pollId);
0 ignored issues
show
Bug introduced by
It seems like $pollId can also be of type null; however, parameter $pollId of OCA\Polls\Db\VoteMapper::findByPoll() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

99
			$votes = $this->voteMapper->findByPoll(/** @scrutinizer ignore-type */ $pollId);
Loading history...
100
			$countParticipants = count($this->voteMapper->findParticipantsByPoll($pollId));
0 ignored issues
show
Bug introduced by
It seems like $pollId can also be of type null; however, parameter $pollId of OCA\Polls\Db\VoteMapper::findParticipantsByPoll() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

100
			$countParticipants = count($this->voteMapper->findParticipantsByPoll(/** @scrutinizer ignore-type */ $pollId));
Loading history...
101
102
			foreach ($options as $option) {
103
				$option->yes = count(
104
					array_filter($votes, function ($vote) use ($option) {
105
						if ($vote->getVoteOptionText() === $option->getPollOptionText()
106
							&& $vote->getVoteAnswer() === 'yes') {
107
							return $vote;
108
						}
109
					})
110
				);
111
112
				$option->realno = count(
113
					array_filter($votes, function ($vote) use ($option) {
114
						if ($vote->getVoteOptionText() === $option->getPollOptionText()
115
							&& $vote->getVoteAnswer() === 'no') {
116
							return $vote;
117
						}
118
					})
119
				);
120
121
				$option->maybe = count(
122
					array_filter($votes, function ($vote) use ($option) {
123
						if ($vote->getVoteOptionText() === $option->getPollOptionText()
124
							&& $vote->getVoteAnswer() === 'maybe') {
125
							return $vote;
126
						}
127
					})
128
				);
129
130
				$option->isBookedUp = $poll->getOptionLimit() ? $poll->getOptionLimit() <= $option->yes : false;
131
132
				if (!$this->acl->isAllowed(Acl::PERMISSION_SEE_RESULTS)) {
133
					$option->yes = 0;
134
					$option->no = 0;
135
					$option->maybe = 0;
136
					$option->realNo = 0;
137
				} else {
138
					$option->no = $countParticipants - $option->maybe - $option->yes;
139
				}
140
			}
141
			// hide booked up options except the user has edit permission
142
			if ($poll->getHideBookedUp() && !$this->acl->isAllowed(Acl::PERMISSION_EDIT)) {
0 ignored issues
show
Bug introduced by
The method getHideBookedUp() does not exist on OCA\Polls\Db\Poll. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

142
			if ($poll->/** @scrutinizer ignore-call */ getHideBookedUp() && !$this->acl->isAllowed(Acl::PERMISSION_EDIT)) {
Loading history...
143
144
				// Thats an ugly solution, but for now, it seems to work
145
				// Optimization proposals are welcome
146
147
				// If the user opted in, do not hide them
148
				// First: Find votes, where the user voted yes or maybe
149
				$userId = $this->acl->getUserId();
150
				$exceptVotes = array_filter($votes, function ($vote) use ($userId) {
151
					if ($vote->getUserId() === $userId && in_array($vote->getVoteAnswer(), ['yes', 'maybe'])) {
152
						return $vote;
153
					}
154
				});
155
156
				// Second: Extract only the vote option texts to an array
157
				$exceptVotes = array_values(array_map(function ($vote) {
158
					return $vote->getVoteOptionText();
159
				}, $exceptVotes));
160
161
				// Third: Reduce options to options, which are not booked up or
162
				// the user has opted in via yes or maybe answer
163
				$options = array_filter($options, function ($option) use ($exceptVotes) {
164
					if (!$option->getIsBookedUp() || in_array($option->getPollOptionText(), $exceptVotes)) {
165
						return $option;
166
					}
167
				});
168
			} elseif ($this->acl->isAllowed(Acl::PERMISSION_SEE_RESULTS)) {
169
170
				// sort array by yes and maybe votes
171
				usort($options, function ($a, $b) {
172
					$diff = $b->yes - $a->yes;
173
					return ($diff !== 0) ? $diff : $b->maybe - $a->maybe;
174
				});
175
176
				// calculate the rank
177
				for ($i = 0; $i < count($options); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
178
					if ($i > 0 && $options[$i]->yes === $options[$i - 1]->yes && $options[$i]->maybe === $options[$i - 1]->maybe) {
179
						$options[$i]->rank = $options[$i - 1]->rank;
180
					} else {
181
						$options[$i]->rank = $i + 1;
182
					}
183
				}
184
185
				// restore original order
186
				usort($options, function ($a, $b) {
187
					return $a->getOrder() - $b->getOrder();
188
				});
189
			}
190
191
			return array_values($options);
192
		} catch (DoesNotExistException $e) {
193
			return [];
194
		}
195
	}
196
197
	/**
198
	 * 	 * Get option
199
	 *
200
	 * @return Option
201
	 */
202
	public function get(int $optionId): Option {
203
		$this->acl->setPollId($this->optionMapper->find($optionId)->getPollId())->request(Acl::PERMISSION_VIEW);
204
205
		if (!$this->acl->isAllowed(Acl::PERMISSION_VIEW)) {
206
			throw new NotAuthorizedException;
207
		}
208
209
		return $this->optionMapper->find($optionId);
210
	}
211
212
213
	/**
214
	 * 	 * Add a new option
215
	 *
216
	 * @return Option
217
	 */
218
	public function add(int $pollId, int $timestamp = 0, string $pollOptionText = '', ?int $duration = 0): Option {
219
		$this->acl->setPollId($pollId)->request(Acl::PERMISSION_EDIT);
220
		$this->option = new Option();
221
		$this->option->setPollId($pollId);
222
		$this->option->setOrder($this->getHighestOrder($this->option->getPollId()) + 1);
223
		$this->setOption($timestamp, $pollOptionText, $duration);
224
225
		try {
226
			$this->option = $this->optionMapper->insert($this->option);
227
			$this->watchService->writeUpdate($this->option->getPollId(), Watch::OBJECT_OPTIONS);
228
		} catch (UniqueConstraintViolationException $e) {
229
			throw new DuplicateEntryException('This option already exists');
230
		}
231
		return $this->option;
232
	}
233
234
	/**
235
	 * 	 * Update option
236
	 *
237
	 * @return Option
238
	 */
239
	public function update(int $optionId, int $timestamp = 0, ?string $pollOptionText = '', ?int $duration = 0): Option {
240
		$this->option = $this->optionMapper->find($optionId);
241
		$this->acl->setPollId($this->option->getPollId())->request(Acl::PERMISSION_EDIT);
242
		$this->setOption($timestamp, $pollOptionText, $duration);
243
244
		$this->option = $this->optionMapper->update($this->option);
245
		$this->watchService->writeUpdate($this->option->getPollId(), Watch::OBJECT_OPTIONS);
246
		return $this->option;
247
	}
248
249
	/**
250
	 * 	 * Delete option
251
	 *
252
	 * @return Option
253
	 */
254
	public function delete(int $optionId): Option {
255
		$this->option = $this->optionMapper->find($optionId);
256
		$this->acl->setPollId($this->option->getPollId())->request(Acl::PERMISSION_EDIT);
257
		$this->optionMapper->delete($this->option);
258
		$this->watchService->writeUpdate($this->option->getPollId(), Watch::OBJECT_OPTIONS);
259
260
		return $this->option;
261
	}
262
263
	/**
264
	 * 	 * Switch optoin confirmation
265
	 *
266
	 * @return Option
267
	 */
268
	public function confirm(int $optionId): Option {
269
		$this->option = $this->optionMapper->find($optionId);
270
		$this->acl->setPollId($this->option->getPollId())->request(Acl::PERMISSION_EDIT);
271
272
		$this->option->setConfirmed($this->option->getConfirmed() ? 0 : time());
273
		$this->option = $this->optionMapper->update($this->option);
274
		$this->watchService->writeUpdate($this->option->getPollId(), Watch::OBJECT_OPTIONS);
275
		return $this->option;
276
	}
277
278
	/**
279
	 * 	 * Make a sequence of date poll options
280
	 *
281
	 * @return Option[]
282
	 *
283
	 * @psalm-return array<array-key, Option>
284
	 */
285
	public function sequence(int $optionId, int $step, string $unit, int $amount): array {
286
		$baseDate = new DateTime;
287
		$this->option = $this->optionMapper->find($optionId);
288
		$this->acl->setPollId($this->option->getPollId())->request(Acl::PERMISSION_EDIT);
289
290
		if ($step === 0) {
291
			return $this->optionMapper->findByPoll($this->option->getPollId());
292
		}
293
294
		$baseDate->setTimestamp($this->option->getTimestamp());
295
296
		for ($i = 0; $i < $amount; $i++) {
297
			$clonedOption = new Option();
298
			$clonedOption->setPollId($this->option->getPollId());
299
			$clonedOption->setDuration($this->option->getDuration());
300
			$clonedOption->setConfirmed(0);
301
			$clonedOption->setTimestamp($baseDate->modify($step . ' ' . $unit)->getTimestamp());
302
			$clonedOption->setOrder($clonedOption->getTimestamp());
303
			$clonedOption->setPollOptionText($baseDate->format('c'));
304
			try {
305
				$this->optionMapper->insert($clonedOption);
306
			} catch (UniqueConstraintViolationException $e) {
307
				\OC::$server->getLogger()->warning('skip adding ' . $baseDate->format('c') . 'for pollId' . $this->option->getPollId() . '. Option alredy exists.');
308
			}
309
		}
310
		$this->watchService->writeUpdate($this->option->getPollId(), Watch::OBJECT_OPTIONS);
311
		return $this->optionMapper->findByPoll($this->option->getPollId());
312
	}
313
314
	/**
315
	 * 	 * Copy options from $fromPoll to $toPoll
316
	 *
317
	 * @return Option[]
318
	 *
319
	 * @psalm-return array<array-key, Option>
320
	 */
321
	public function clone(int $fromPollId, int $toPollId): array {
322
		$this->acl->setPollId($fromPollId);
323
324
		foreach ($this->optionMapper->findByPoll($fromPollId) as $origin) {
325
			$option = new Option();
326
			$option->setPollId($toPollId);
327
			$option->setConfirmed(0);
328
			$option->setPollOptionText($origin->getPollOptionText());
329
			$option->setTimestamp($origin->getTimestamp());
330
			$option->setDuration($origin->getDuration());
331
			$option->setOrder($option->getOrder());
332
			$this->optionMapper->insert($option);
333
		}
334
335
		return $this->optionMapper->findByPoll($toPollId);
336
	}
337
338
	/**
339
	 * 	 * Reorder options with the order specified by $options
340
	 *
341
	 * @return Option[]
342
	 *
343
	 * @psalm-return array<array-key, Option>
344
	 */
345
	public function reorder(int $pollId, array $options): array {
346
		try {
347
			$poll = $this->pollMapper->find($pollId);
348
			$this->acl->setPoll($poll)->request(Acl::PERMISSION_EDIT);
349
350
			if ($poll->getType() === Poll::TYPE_DATE) {
351
				throw new BadRequestException("Not allowed in date polls");
352
			}
353
		} catch (DoesNotExistException $e) {
354
			throw new NotAuthorizedException;
355
		}
356
357
		$i = 0;
358
		foreach ($options as $option) {
359
			$this->option = $this->optionMapper->find($option['id']);
360
			if ($pollId === intval($this->option->getPollId())) {
361
				$this->option->setOrder(++$i);
362
				$this->optionMapper->update($this->option);
363
			}
364
		}
365
366
		$this->watchService->writeUpdate($pollId, Watch::OBJECT_OPTIONS);
367
		return $this->optionMapper->findByPoll($pollId);
368
	}
369
370
	/**
371
	 * 	 * Change order for $optionId and reorder the options
372
	 *
373
	 * @NoAdminRequired
374
	 *
375
	 * @return Option[]
376
	 *
377
	 * @psalm-return array<array-key, Option>
378
	 */
379
	public function setOrder(int $optionId, int $newOrder): array {
380
		try {
381
			$this->option = $this->optionMapper->find($optionId);
382
			$poll = $this->pollMapper->find($this->option->getPollId());
383
			$this->acl->setPoll($poll)->request(Acl::PERMISSION_EDIT);
384
385
			if ($poll->getType() === Poll::TYPE_DATE) {
386
				throw new BadRequestException("Not allowed in date polls");
387
			}
388
		} catch (DoesNotExistException $e) {
389
			throw new NotAuthorizedException;
390
		}
391
392
		if ($newOrder < 1) {
393
			$newOrder = 1;
394
		} elseif ($newOrder > $this->getHighestOrder($poll->getId())) {
395
			$newOrder = $this->getHighestOrder($poll->getId());
396
		}
397
398
		foreach ($this->optionMapper->findByPoll($poll->getId()) as $option) {
399
			$option->setOrder($this->moveModifier($this->option->getOrder(), $newOrder, $option->getOrder()));
400
			$this->optionMapper->update($option);
401
		}
402
403
		$this->watchService->writeUpdate($this->option->getPollId(), Watch::OBJECT_OPTIONS);
404
		return $this->optionMapper->findByPoll($this->option->getPollId());
405
	}
406
407
	/**
408
	 * 	 * moveModifier - evaluate new order
409
	 * 	 * depending on the old and the new position of a moved array item
410
	 * 	 * $moveFrom - old position of the moved item
411
	 * 	 * $moveTo   - target posotion of the moved item
412
	 * 	 * $value    - current position of the current item
413
	 * 	 * Returns the modified new new position of the current item
414
	 *
415
	 * @return int
416
	 */
417
	private function moveModifier(int $moveFrom, int $moveTo, int $currentPosition): int {
418
		$moveModifier = 0;
419
		if ($moveFrom < $currentPosition && $currentPosition <= $moveTo) {
420
			// moving forward
421
			$moveModifier = -1;
422
		} elseif ($moveTo <= $currentPosition && $currentPosition < $moveFrom) {
423
			//moving backwards
424
			$moveModifier = 1;
425
		} elseif ($moveFrom === $currentPosition) {
426
			return $moveTo;
427
		}
428
		return $currentPosition + $moveModifier;
429
	}
430
431
	/**
432
	 * Set option entities validated
433
	 */
434
	private function setOption(int $timestamp = 0, ?string $pollOptionText = '', ?int $duration = 0): void {
435
		$poll = $this->pollMapper->find($this->option->getPollId());
436
437
		if ($poll->getType() === Poll::TYPE_DATE) {
438
			$this->option->setTimestamp($timestamp);
439
			$this->option->setOrder($timestamp);
440
			$this->option->setDuration($duration);
441
			if ($duration === 0) {
442
				$this->option->setPollOptionText(date('c', $timestamp));
443
			} elseif ($duration > 0) {
444
				$this->option->setPollOptionText(date('c', $timestamp) .' - ' . date('c', $timestamp + $duration));
445
			} else {
446
				$this->option->setPollOptionText($pollOptionText);
447
			}
448
		} else {
449
			$this->option->setPollOptionText($pollOptionText);
450
		}
451
	}
452
453
	/**
454
	 * 	 * Get the highest order number in $pollId
455
	 * 	 * Return Highest order number
456
	 *
457
	 * @return int
458
	 */
459
	private function getHighestOrder(int $pollId): int {
460
		$highestOrder = 0;
461
		foreach ($this->optionMapper->findByPoll($pollId) as $option) {
462
			$highestOrder = ($option->getOrder() > $highestOrder) ? $option->getOrder() : $highestOrder;
463
		}
464
		return $highestOrder;
465
	}
466
}
467