1<?php
2
3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
4// See the LICENCE file in the repository root for full licence text.
5
6namespace App\Models\Forum;
7
8use App\Traits\Validatable;
9use Carbon\Carbon;
10use DB;
11
12class TopicPoll
13{
14 use Validatable;
15
16 private $topic;
17 private $validated = false;
18 private $params = [
19 'hide_results' => false,
20 'length_days' => 0,
21 'max_options' => 1,
22 'options' => [],
23 'title' => null,
24 'vote_change' => false,
25 ];
26 private $votedBy = [];
27
28 public function __get(string $field)
29 {
30 return $this->params[$field];
31 }
32
33 public function canEdit()
34 {
35 return $this->topic->topic_time > Carbon::now()->subHours($GLOBALS['cfg']['osu']['forum']['poll_edit_hours']);
36 }
37
38 public function exists()
39 {
40 return present($this->topic->poll_title);
41 }
42
43 public function fill($params)
44 {
45 $this->params = [
46 ...$this->params,
47 ...$params,
48 ];
49 $this->validated = false;
50
51 return $this;
52 }
53
54 public function isOpen()
55 {
56 if ($this->topic === null) {
57 return false;
58 }
59
60 return $this->topic->pollEnd() === null || $this->topic->pollEnd()->isFuture();
61 }
62
63 public function isValid($revalidate = false)
64 {
65 if (!$this->validated || $revalidate) {
66 $this->validated = true;
67 $this->validationErrors()->reset();
68
69 if (!present($this->params['title'])) {
70 $this->validationErrors()->add('title', 'required');
71 }
72
73 $this->validateFieldLength(255, 'title');
74
75 if (count($this->params['options']) > count(array_unique($this->params['options']))) {
76 $this->validationErrors()->add('options', '.duplicate_options');
77 }
78
79 if (count($this->params['options']) < 2) {
80 $this->validationErrors()->add('options', '.minimum_two_options');
81 }
82
83 if (count($this->params['options']) > 10) {
84 $this->validationErrors()->add('options', '.too_many_options');
85 }
86
87 if ($this->params['max_options'] < 1) {
88 $this->validationErrors()->add('max_options', '.minimum_one_selection');
89 }
90
91 if ($this->params['max_options'] > count($this->params['options'])) {
92 $this->validationErrors()->add('max_options', '.invalid_max_options');
93 }
94
95 if ($this->params['hide_results'] && $this->params['length_days'] === 0) {
96 $this->validationErrors()->add('hide_results', '.hiding_results_forever');
97 }
98
99 if ($this->topic !== null && $this->topic->exists && !$this->canEdit()) {
100 $this->validationErrors()->add(
101 'edit',
102 '.grace_period_expired',
103 ['limit' => $GLOBALS['cfg']['osu']['forum']['poll_edit_hours']]
104 );
105 }
106 }
107
108 return $this->validationErrors()->isEmpty();
109 }
110
111 public function save()
112 {
113 if (!$this->isValid()) {
114 return false;
115 }
116
117 return DB::transaction(function () {
118 $this->topic->update([
119 'poll_title' => $this->params['title'],
120 'poll_start' => Carbon::now(),
121 'poll_length' => $this->params['length_days'] * 3600 * 24,
122 'poll_max_options' => $this->params['max_options'],
123 'poll_vote_change' => $this->params['vote_change'],
124 'poll_hide_results' => $this->params['hide_results'],
125 ]);
126
127 $this
128 ->topic
129 ->pollVotes()
130 ->delete();
131
132 $this
133 ->topic
134 ->pollOptions()
135 ->delete();
136
137 foreach ($this->params['options'] as $index => $value) {
138 PollOption::create([
139 'topic_id' => $this->topic->topic_id,
140 'poll_option_id' => $index,
141 'poll_option_text' => $value,
142 ]);
143 }
144
145 return true;
146 });
147 }
148
149 public function setTopic($topic)
150 {
151 $this->topic = $topic;
152
153 return $this;
154 }
155
156 /**
157 * Get the aggregate vote count of this poll, or `0` if the poll doesn't exist. If the poll
158 * allows selecting more than one option, this may be greater than the number of users who voted.
159 */
160 public function totalVoteCount(): int
161 {
162 return $this->exists()
163 ? $this->topic->pollOptions->sum('poll_option_total')
164 : 0;
165 }
166
167 public function validationErrorsTranslationPrefix(): string
168 {
169 return 'forum.topic_poll';
170 }
171
172 public function votedBy($user)
173 {
174 if ($user === null) {
175 return false;
176 }
177
178 if ($this->topic === null) {
179 return false;
180 }
181
182 $userId = $user->getKey();
183
184 if (!isset($this->votedBy[$userId])) {
185 $this->votedBy[$userId] = $this->topic->pollVotes()->where('vote_user_id', $userId)->exists();
186 }
187
188 return $this->votedBy[$userId];
189 }
190}