Passed
Push — master ( 5e04fe...8bd5e6 )
by René
04:45
created

OptionService   F

Complexity

Total Complexity 62

Size/Duplication

Total Lines 459
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 4
Bugs 3 Features 0
Metric Value
wmc 62
eloc 198
c 4
b 3
f 0
dl 0
loc 459
ccs 0
cts 199
cp 0
rs 3.44

18 Methods

Rating   Name   Duplication   Size   Complexity  
A add() 0 14 2
A moveModifier() 0 12 6
A confirm() 0 8 2
A getHighestOrder() 0 6 3
A clone() 0 15 2
A update() 0 8 1
A sequence() 0 27 4
A delete() 0 7 1
A __construct() 0 14 1
A list() 0 26 6
A filterBookedUp() 0 4 2
A getUsersVotes() 0 16 3
A get() 0 4 1
A calculateRanks() 0 20 6
A setOrder() 0 26 6
A reorder() 0 23 5
A setOption() 0 16 4
B calculateVotes() 0 32 7

How to fix   Complexity   

Complex Class

Complex classes like OptionService often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use OptionService, and based on these observations, apply Extract Interface, too.

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)->request(Acl::PERMISSION_VIEW);
100
			$pollId = $this->acl->getPollId();
101
		} else {
102
			$this->acl->setPollId($pollId)->request(Acl::PERMISSION_VIEW);
103
		}
104
105
		try {
106
			$this->poll = $this->pollMapper->find($pollId);
107
			$this->options = $this->optionMapper->findByPoll($pollId);
108
			$this->votes = $this->voteMapper->findByPoll($pollId);
109
			$this->countParticipants = count($this->voteMapper->findParticipantsByPoll($pollId));
110
111
			$this->calculateVotes();
112
113
			if ($this->poll->getHideBookedUp() && !$this->acl->isAllowed(Acl::PERMISSION_EDIT)) {
114
				// hide booked up options except the user has edit permission
115
				$this->filterBookedUp();
116
			} elseif ($this->acl->isAllowed(Acl::PERMISSION_SEE_RESULTS)) {
117
				$this->calculateRanks();
118
			}
119
120
			return array_values($this->options);
121
		} catch (DoesNotExistException $e) {
122
			return [];
123
		}
124
	}
125
126
	/**
127
	 * 	 * Get option
128
	 *
129
	 * @return Option
130
	 */
131
	public function get(int $optionId): Option {
132
		$this->acl->setPollId($this->optionMapper->find($optionId)->getPollId())
133
			->request(Acl::PERMISSION_VIEW);
134
		return $this->optionMapper->find($optionId);
135
	}
136
137
138
	/**
139
	 * 	 * Add a new option
140
	 *
141
	 * @return Option
142
	 */
143
	public function add(int $pollId, int $timestamp = 0, string $pollOptionText = '', ?int $duration = 0): Option {
144
		$this->acl->setPollId($pollId)->request(Acl::PERMISSION_EDIT);
145
		$this->option = new Option();
146
		$this->option->setPollId($pollId);
147
		$this->option->setOrder($this->getHighestOrder($this->option->getPollId()) + 1);
148
		$this->setOption($timestamp, $pollOptionText, $duration);
149
150
		try {
151
			$this->option = $this->optionMapper->insert($this->option);
152
			$this->watchService->writeUpdate($this->option->getPollId(), Watch::OBJECT_OPTIONS);
153
		} catch (UniqueConstraintViolationException $e) {
154
			throw new DuplicateEntryException('This option already exists');
155
		}
156
		return $this->option;
157
	}
158
159
	/**
160
	 * 	 * Update option
161
	 *
162
	 * @return Option
163
	 */
164
	public function update(int $optionId, int $timestamp = 0, ?string $pollOptionText = '', ?int $duration = 0): Option {
165
		$this->option = $this->optionMapper->find($optionId);
166
		$this->acl->setPollId($this->option->getPollId())->request(Acl::PERMISSION_EDIT);
167
		$this->setOption($timestamp, $pollOptionText, $duration);
168
169
		$this->option = $this->optionMapper->update($this->option);
170
		$this->watchService->writeUpdate($this->option->getPollId(), Watch::OBJECT_OPTIONS);
171
		return $this->option;
172
	}
173
174
	/**
175
	 * 	 * Delete option
176
	 *
177
	 * @return Option
178
	 */
179
	public function delete(int $optionId): Option {
180
		$this->option = $this->optionMapper->find($optionId);
181
		$this->acl->setPollId($this->option->getPollId())->request(Acl::PERMISSION_EDIT);
182
		$this->optionMapper->delete($this->option);
183
		$this->watchService->writeUpdate($this->option->getPollId(), Watch::OBJECT_OPTIONS);
184
185
		return $this->option;
186
	}
187
188
	/**
189
	 * 	 * Switch optoin confirmation
190
	 *
191
	 * @return Option
192
	 */
193
	public function confirm(int $optionId): Option {
194
		$this->option = $this->optionMapper->find($optionId);
195
		$this->acl->setPollId($this->option->getPollId())->request(Acl::PERMISSION_EDIT);
196
197
		$this->option->setConfirmed($this->option->getConfirmed() ? 0 : time());
198
		$this->option = $this->optionMapper->update($this->option);
199
		$this->watchService->writeUpdate($this->option->getPollId(), Watch::OBJECT_OPTIONS);
200
		return $this->option;
201
	}
202
203
	/**
204
	 * 	 * Make a sequence of date poll options
205
	 * @param int $optionId
206
	 * @param int $step - The step for creating the sequence
207
	 * @param string $unit - The timeunit (year, month, ...)
208
	 * @param int $amount - Number of sequence items to create
209
	 *
210
	 * @return Option[]
211
	 *
212
	 * @psalm-return array<array-key, Option>
213
	 */
214
	public function sequence(int $optionId, int $step, string $unit, int $amount): array {
215
		$baseDate = new DateTime;
216
		$this->option = $this->optionMapper->find($optionId);
217
		$this->acl->setPollId($this->option->getPollId())->request(Acl::PERMISSION_EDIT);
218
219
		if ($step === 0) {
220
			return $this->optionMapper->findByPoll($this->option->getPollId());
221
		}
222
223
		$baseDate->setTimestamp($this->option->getTimestamp());
224
225
		for ($i = 0; $i < $amount; $i++) {
226
			$clonedOption = new Option();
227
			$clonedOption->setPollId($this->option->getPollId());
228
			$clonedOption->setDuration($this->option->getDuration());
229
			$clonedOption->setConfirmed(0);
230
			$clonedOption->setTimestamp($baseDate->modify($step . ' ' . $unit)->getTimestamp());
231
			$clonedOption->setOrder($clonedOption->getTimestamp());
232
			$clonedOption->setPollOptionText($baseDate->format('c'));
233
			try {
234
				$this->optionMapper->insert($clonedOption);
235
			} catch (UniqueConstraintViolationException $e) {
236
				\OC::$server->getLogger()->warning('skip adding ' . $baseDate->format('c') . 'for pollId' . $this->option->getPollId() . '. Option alredy exists.');
237
			}
238
		}
239
		$this->watchService->writeUpdate($this->option->getPollId(), Watch::OBJECT_OPTIONS);
240
		return $this->optionMapper->findByPoll($this->option->getPollId());
241
	}
242
243
	/**
244
	 * 	 * Copy options from $fromPoll to $toPoll
245
	 *
246
	 * @return Option[]
247
	 *
248
	 * @psalm-return array<array-key, Option>
249
	 */
250
	public function clone(int $fromPollId, int $toPollId): array {
251
		$this->acl->setPollId($fromPollId);
252
253
		foreach ($this->optionMapper->findByPoll($fromPollId) as $origin) {
254
			$option = new Option();
255
			$option->setPollId($toPollId);
256
			$option->setConfirmed(0);
257
			$option->setPollOptionText($origin->getPollOptionText());
258
			$option->setTimestamp($origin->getTimestamp());
259
			$option->setDuration($origin->getDuration());
260
			$option->setOrder($option->getOrder());
261
			$this->optionMapper->insert($option);
262
		}
263
264
		return $this->optionMapper->findByPoll($toPollId);
265
	}
266
267
	/**
268
	 * Reorder options with the order specified by $options
269
	 *
270
	 * @return Option[]
271
	 *
272
	 * @psalm-return array<array-key, Option>
273
	 */
274
	public function reorder(int $pollId, array $options): array {
275
		try {
276
			$this->poll = $this->pollMapper->find($pollId);
277
			$this->acl->setPoll($this->poll)->request(Acl::PERMISSION_EDIT);
278
279
			if ($this->poll->getType() === Poll::TYPE_DATE) {
280
				throw new BadRequestException("Not allowed in date polls");
281
			}
282
		} catch (DoesNotExistException $e) {
283
			throw new NotAuthorizedException;
284
		}
285
286
		$i = 0;
287
		foreach ($options as $option) {
288
			$this->option = $this->optionMapper->find($option['id']);
289
			if ($pollId === intval($this->option->getPollId())) {
290
				$this->option->setOrder(++$i);
291
				$this->optionMapper->update($this->option);
292
			}
293
		}
294
295
		$this->watchService->writeUpdate($pollId, Watch::OBJECT_OPTIONS);
296
		return $this->optionMapper->findByPoll($pollId);
297
	}
298
299
	/**
300
	 * Change order for $optionId and reorder the options
301
	 *
302
	 * @return Option[]
303
	 *
304
	 * @psalm-return array<array-key, Option>
305
	 */
306
	public function setOrder(int $optionId, int $newOrder): array {
307
		try {
308
			$this->option = $this->optionMapper->find($optionId);
309
			$this->poll = $this->pollMapper->find($this->option->getPollId());
310
			$this->acl->setPoll($this->poll)->request(Acl::PERMISSION_EDIT);
311
312
			if ($this->poll->getType() === Poll::TYPE_DATE) {
313
				throw new BadRequestException("Not allowed in date polls");
314
			}
315
		} catch (DoesNotExistException $e) {
316
			throw new NotAuthorizedException;
317
		}
318
319
		if ($newOrder < 1) {
320
			$newOrder = 1;
321
		} elseif ($newOrder > $this->getHighestOrder($this->poll->getId())) {
322
			$newOrder = $this->getHighestOrder($this->poll->getId());
323
		}
324
325
		foreach ($this->optionMapper->findByPoll($this->poll->getId()) as $option) {
326
			$option->setOrder($this->moveModifier($this->option->getOrder(), $newOrder, $option->getOrder()));
327
			$this->optionMapper->update($option);
328
		}
329
330
		$this->watchService->writeUpdate($this->option->getPollId(), Watch::OBJECT_OPTIONS);
331
		return $this->optionMapper->findByPoll($this->option->getPollId());
332
	}
333
334
	/**
335
	 * moveModifier - evaluate new order depending on the old and
336
	 * the new position of a moved array item
337
	 * @param int $moveFrom - old position of the moved item
338
	 * @param int $moveTo - target posotion of the moved item
339
	 * @param int $currentPosition - current position of the current item
340
	 *
341
	 * @return int - The modified new new position of the current item
342
	 */
343
	private function moveModifier(int $moveFrom, int $moveTo, int $currentPosition): int {
344
		$moveModifier = 0;
345
		if ($moveFrom < $currentPosition && $currentPosition <= $moveTo) {
346
			// moving forward
347
			$moveModifier = -1;
348
		} elseif ($moveTo <= $currentPosition && $currentPosition < $moveFrom) {
349
			//moving backwards
350
			$moveModifier = 1;
351
		} elseif ($moveFrom === $currentPosition) {
352
			return $moveTo;
353
		}
354
		return $currentPosition + $moveModifier;
355
	}
356
357
	/**
358
	 * Set option entities validated
359
	 *
360
	 * @return void
361
	 */
362
	private function setOption(int $timestamp = 0, ?string $pollOptionText = '', ?int $duration = 0): void {
363
		$this->poll = $this->pollMapper->find($this->option->getPollId());
364
365
		if ($this->poll->getType() === Poll::TYPE_DATE) {
366
			$this->option->setTimestamp($timestamp);
367
			$this->option->setOrder($timestamp);
368
			$this->option->setDuration($duration);
369
			if ($duration === 0) {
370
				$this->option->setPollOptionText(date('c', $timestamp));
371
			} elseif ($duration > 0) {
372
				$this->option->setPollOptionText(date('c', $timestamp) . ' - ' . date('c', $timestamp + $duration));
373
			} else {
374
				$this->option->setPollOptionText($pollOptionText);
375
			}
376
		} else {
377
			$this->option->setPollOptionText($pollOptionText);
378
		}
379
	}
380
381
	/**
382
	 * Get all voteOptionTexts of the options, the user opted in
383
	 * with yes or maybe
384
	 *
385
	 * @return array
386
	 */
387
	private function getUsersVotes() {
388
		// Thats an ugly solution, but for now, it seems to work
389
		// Optimization proposals are welcome
390
391
		// First: Find votes, where the user voted yes or maybe
392
		$userId = $this->acl->getUserId();
393
		$exceptVotes = array_filter($this->votes, function ($vote) use ($userId) {
394
			if ($vote->getUserId() === $userId && in_array($vote->getVoteAnswer(), ['yes', 'maybe'])) {
395
				return $vote;
396
			}
397
		});
398
399
		// Second: Extract only the vote option texts to an array
400
		return array_values(array_map(function ($vote) {
401
			return $vote->getVoteOptionText();
402
		}, $exceptVotes));
403
	}
404
405
	/**
406
	 * Remove booked up options, because they are not votable
407
	 *
408
	 * @return void
409
	 */
410
	private function filterBookedUp() {
411
		$exceptVotes = $this->getUsersVotes();
412
		$this->options = array_filter($this->options, function ($option) use ($exceptVotes) {
413
			return (!$option->getIsBookedUp() || in_array($option->getPollOptionText(), $exceptVotes));
414
		});
415
	}
416
417
	/**
418
	 * Calculate the votes of each option and determines if the option is booked up
419
	 * - unvoted counts as no
420
	 * - realNo reports the actually opted out votes
421
	 *
422
	 * @return void
423
	 */
424
	private function calculateVotes() {
425
		foreach ($this->options as $option) {
426
			$option->yes = count(
427
				array_filter($this->votes, function ($vote) use ($option) {
428
					return ($vote->getVoteOptionText() === $option->getPollOptionText()
429
						&& $vote->getVoteAnswer() === 'yes') ;
430
				})
431
			);
432
433
			$option->realNo = count(
434
				array_filter($this->votes, function ($vote) use ($option) {
435
					return ($vote->getVoteOptionText() === $option->getPollOptionText()
436
						&& $vote->getVoteAnswer() === 'no');
437
				})
438
			);
439
440
			$option->maybe = count(
441
				array_filter($this->votes, function ($vote) use ($option) {
442
					return ($vote->getVoteOptionText() === $option->getPollOptionText()
443
						&& $vote->getVoteAnswer() === 'maybe');
444
				})
445
			);
446
447
			$option->isBookedUp = $this->poll->getOptionLimit() ? $this->poll->getOptionLimit() <= $option->yes : false;
448
449
			if (!$this->acl->isAllowed(Acl::PERMISSION_SEE_RESULTS)) {
450
				$option->yes = 0;
451
				$option->no = 0;
452
				$option->maybe = 0;
453
				$option->realNo = 0;
454
			} else {
455
				$option->no = $this->countParticipants - $option->maybe - $option->yes;
456
			}
457
		}
458
	}
459
460
	/**
461
	 * Calculate the rank of each option based on the
462
	 * yes and maybe votes and recognize equal ranked options
463
	 *
464
	 * @return void
465
	 */
466
	private function calculateRanks() {
467
		// sort array by yes and maybe votes
468
		usort($this->options, function ($a, $b) {
469
			$diff = $b->yes - $a->yes;
470
			return ($diff !== 0) ? $diff : $b->maybe - $a->maybe;
471
		});
472
473
		// calculate the rank
474
		$count = count($this->options);
475
		for ($i = 0; $i < $count; $i++) {
476
			if ($i > 0 && $this->options[$i]->yes === $this->options[$i - 1]->yes && $this->options[$i]->maybe === $this->options[$i - 1]->maybe) {
477
				$this->options[$i]->rank = $this->options[$i - 1]->rank;
478
			} else {
479
				$this->options[$i]->rank = $i + 1;
480
			}
481
		}
482
483
		// restore original order
484
		usort($this->options, function ($a, $b) {
485
			return $a->getOrder() - $b->getOrder();
486
		});
487
	}
488
489
	/**
490
	 * 	 * Get the highest order number in $pollId
491
	 * 	 * Return Highest order number
492
	 *
493
	 * @return int
494
	 */
495
	private function getHighestOrder(int $pollId): int {
496
		$highestOrder = 0;
497
		foreach ($this->optionMapper->findByPoll($pollId) as $option) {
498
			$highestOrder = ($option->getOrder() > $highestOrder) ? $option->getOrder() : $highestOrder;
499
		}
500
		return $highestOrder;
501
	}
502
}
503