Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
50.85% |
30 / 59 |
|
18.18% |
4 / 22 |
CRAP | |
0.00% |
0 / 1 |
TaskSet | |
50.85% |
30 / 59 |
|
18.18% |
4 / 22 |
128.87 | |
0.00% |
0 / 1 |
__construct | |
16.67% |
1 / 6 |
|
0.00% |
0 / 1 |
1.58 | |||
getIterator | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
count | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
offsetExists | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
offsetGet | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
offsetSet | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
offsetUnset | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTotalCount | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getOffset | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getDebugData | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setDebugData | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getFilters | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
truncate | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
randomSort | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
filtersEqual | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getQualityGateConfig | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setQualityGateConfigForTaskType | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setQualityGateConfig | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getInvalidTasks | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
toJsonArray | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
1 | |||
newFromJsonArray | |
92.86% |
13 / 14 |
|
0.00% |
0 / 1 |
4.01 | |||
containsPage | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | |
3 | namespace GrowthExperiments\NewcomerTasks\Task; |
4 | |
5 | use ArrayAccess; |
6 | use ArrayIterator; |
7 | use Countable; |
8 | use IteratorAggregate; |
9 | use LogicException; |
10 | use MediaWiki\Json\JsonUnserializable; |
11 | use MediaWiki\Json\JsonUnserializableTrait; |
12 | use MediaWiki\Json\JsonUnserializer; |
13 | use MediaWiki\Page\ProperPageIdentity; |
14 | use MediaWiki\Title\Title; |
15 | use OutOfBoundsException; |
16 | use Traversable; |
17 | use Wikimedia\Assert\Assert; |
18 | |
19 | /** |
20 | * A list of task suggestions, which constitute a slice of the total result set of suggestions. |
21 | * Used as a convenience class for queries with limit/offset to pass along some metadata |
22 | * about the full result set (such as offset or total result count). |
23 | */ |
24 | class TaskSet implements IteratorAggregate, Countable, ArrayAccess, JsonUnserializable { |
25 | |
26 | use JsonUnserializableTrait; |
27 | |
28 | /** @var Task[] */ |
29 | private $tasks; |
30 | |
31 | /** @var int Size of the full result set (can be larger than the size of this result set). */ |
32 | private $totalCount; |
33 | |
34 | /** @var int Offset within the full result set. */ |
35 | private $offset; |
36 | |
37 | /** @var array Arbitrary non-task-specific debug data */ |
38 | private $debugData = []; |
39 | |
40 | /** @var TaskSetFilters The task and topic filters used to generate this task set. */ |
41 | private $filters; |
42 | |
43 | /** @var array */ |
44 | private $qualityGateConfig = []; |
45 | |
46 | /** @var array Invalid tasks that are part of this task set. */ |
47 | private $invalidTasks = []; |
48 | |
49 | /** |
50 | * @param Task[] $tasks |
51 | * @param int $totalCount Size of the full result set |
52 | * (can be larger than the size of this result set). |
53 | * @param int $offset Offset within the full result set. |
54 | * @param TaskSetFilters $filters |
55 | * @param Task[] $invalidTasks Tasks that were part of the TaskSet, but are not considered valid. |
56 | */ |
57 | public function __construct( |
58 | array $tasks, $totalCount, $offset, TaskSetFilters $filters, array $invalidTasks = [] |
59 | ) { |
60 | Assert::parameterElementType( Task::class, $tasks, '$tasks' ); |
61 | $this->tasks = array_values( $tasks ); |
62 | $this->invalidTasks = array_values( $invalidTasks ); |
63 | $this->totalCount = $totalCount; |
64 | $this->offset = $offset; |
65 | $this->filters = $filters; |
66 | } |
67 | |
68 | /** |
69 | * @inheritDoc |
70 | * @phan-suppress-next-line PhanTypeMismatchDeclaredReturn |
71 | * @return Traversable|Task[] |
72 | */ |
73 | public function getIterator(): Traversable { |
74 | return new ArrayIterator( $this->tasks ); |
75 | } |
76 | |
77 | /** @inheritDoc */ |
78 | public function count(): int { |
79 | return count( $this->tasks ); |
80 | } |
81 | |
82 | /** @inheritDoc */ |
83 | public function offsetExists( $offset ): bool { |
84 | return array_key_exists( $offset, $this->tasks ); |
85 | } |
86 | |
87 | /** |
88 | * @param int $offset |
89 | * @return Task |
90 | */ |
91 | public function offsetGet( $offset ): Task { |
92 | if ( !array_key_exists( $offset, $this->tasks ) ) { |
93 | throw new OutOfBoundsException( "TaskSet does not have item $offset; max offset: " |
94 | . ( count( $this->tasks ) - 1 ) ); |
95 | } |
96 | return $this->tasks[$offset]; |
97 | } |
98 | |
99 | /** |
100 | * This method cannot be used. |
101 | * @param mixed $offset |
102 | * @param mixed $value |
103 | * @suppress PhanPluginNeverReturnMethod LSP violation |
104 | */ |
105 | public function offsetSet( $offset, $value ): void { |
106 | throw new LogicException( __CLASS__ . ' is read-only' ); |
107 | } |
108 | |
109 | /** |
110 | * This method cannot be used. |
111 | * @param mixed $offset |
112 | * @suppress PhanPluginNeverReturnMethod LSP violation |
113 | */ |
114 | public function offsetUnset( $offset ): void { |
115 | throw new LogicException( __CLASS__ . ' is read-only' ); |
116 | } |
117 | |
118 | /** |
119 | * Size of the full result set (can be larger than the size of this result set), minus any invalidated |
120 | * tasks in the task set. |
121 | * |
122 | * In other words, getTotalCount is the number of suggestions matching some set of conditions |
123 | * while the suggestions returned by iterating the TaskSet are the result of |
124 | * further restricting that set with some limit/offset. |
125 | * @return int |
126 | */ |
127 | public function getTotalCount() { |
128 | return $this->totalCount - count( $this->invalidTasks ); |
129 | } |
130 | |
131 | /** |
132 | * Offset within the full result set. |
133 | * @return int |
134 | */ |
135 | public function getOffset() { |
136 | return $this->offset; |
137 | } |
138 | |
139 | /** |
140 | * Get arbitrary non-task-specific debug data. |
141 | * @return array |
142 | */ |
143 | public function getDebugData(): array { |
144 | return $this->debugData; |
145 | } |
146 | |
147 | /** |
148 | * Set arbitrary non-task-specific debug data. |
149 | * @param array $debugData |
150 | */ |
151 | public function setDebugData( array $debugData ): void { |
152 | $this->debugData = $debugData; |
153 | } |
154 | |
155 | /** |
156 | * @return TaskSetFilters |
157 | */ |
158 | public function getFilters(): TaskSetFilters { |
159 | return $this->filters; |
160 | } |
161 | |
162 | /** |
163 | * Truncate the set of tasks. |
164 | * |
165 | * @param int $limit |
166 | */ |
167 | public function truncate( int $limit ): void { |
168 | if ( $this->count() ) { |
169 | $this->tasks = array_slice( $this->tasks, 0, $limit, true ); |
170 | } |
171 | } |
172 | |
173 | /** |
174 | * Shuffle the tasks. |
175 | */ |
176 | public function randomSort(): void { |
177 | shuffle( $this->tasks ); |
178 | } |
179 | |
180 | /** |
181 | * Compare this TaskSet's filters with another set of filters. |
182 | * @param TaskSetFilters $filters |
183 | * @return bool |
184 | */ |
185 | public function filtersEqual( TaskSetFilters $filters ): bool { |
186 | return json_encode( $this->filters ) === json_encode( $filters ); |
187 | } |
188 | |
189 | /** |
190 | * An array of data to be used in controllers (for now, just client-side in QualityGate.js) for enforcing |
191 | * quality gates. |
192 | * |
193 | * @see modules/ext.growthExperiments.Homepage.SuggestedEdits/QualityGate.js |
194 | * @return array Keys are task type IDs, values are arbitrary data to be used by controllers. |
195 | */ |
196 | public function getQualityGateConfig(): array { |
197 | return $this->qualityGateConfig; |
198 | } |
199 | |
200 | /** |
201 | * @param string $taskTypeId |
202 | * @param array $config |
203 | */ |
204 | public function setQualityGateConfigForTaskType( string $taskTypeId, array $config ): void { |
205 | $this->qualityGateConfig[$taskTypeId] = $config; |
206 | } |
207 | |
208 | /** |
209 | * Set the quality gate data for a TaskSet. Useful when constructing a new TaskSet from an old one. |
210 | * |
211 | * @param array $config |
212 | */ |
213 | public function setQualityGateConfig( array $config ): void { |
214 | $this->qualityGateConfig = $config; |
215 | } |
216 | |
217 | /** |
218 | * @return Task[] |
219 | */ |
220 | public function getInvalidTasks(): array { |
221 | return $this->invalidTasks; |
222 | } |
223 | |
224 | /** @inheritDoc */ |
225 | protected function toJsonArray(): array { |
226 | # T312589 explicitly calling jsonSerialize() will be unnecessary |
227 | # in the future. |
228 | return [ |
229 | 'tasks' => array_map( static function ( Task $task ) { |
230 | return $task->jsonSerialize(); |
231 | }, $this->tasks ), |
232 | 'invalidTasks' => array_map( static function ( Task $task ) { |
233 | return $task->jsonSerialize(); |
234 | }, $this->invalidTasks ), |
235 | 'totalCount' => $this->totalCount, |
236 | 'offset' => $this->offset, |
237 | 'filters' => $this->filters->jsonSerialize(), |
238 | 'qualityGateConfig' => $this->qualityGateConfig, |
239 | // debug data is not worth serializing |
240 | ]; |
241 | } |
242 | |
243 | /** @inheritDoc */ |
244 | public static function newFromJsonArray( JsonUnserializer $unserializer, array $json ) { |
245 | # T312589: In the future JsonCodec will take care of unserializing |
246 | # the values in the $json array itself. |
247 | $tasks = array_map( static function ( $task ) use ( $unserializer ) { |
248 | return $task instanceof Task ? $task : |
249 | $unserializer->unserialize( $task, Task::class ); |
250 | }, $json['tasks'] ); |
251 | $invalidTasks = array_map( static function ( $task ) use ( $unserializer ) { |
252 | return $task instanceof Task ? $task : |
253 | $unserializer->unserialize( $task, Task::class ); |
254 | }, $json['invalidTasks'] ); |
255 | $filters = $json['filters'] instanceof TaskSetFilters ? |
256 | $json['filters'] : |
257 | $unserializer->unserialize( $json['filters'], TaskSetFilters::class ); |
258 | $taskSet = new self( $tasks, $json['totalCount'], $json['offset'], $filters, $invalidTasks ); |
259 | $taskSet->setQualityGateConfig( $json['qualityGateConfig'] ); |
260 | return $taskSet; |
261 | } |
262 | |
263 | /** |
264 | * Check whether the task set contains a task for the specified page |
265 | * |
266 | * @param ProperPageIdentity $page |
267 | * @return bool |
268 | */ |
269 | public function containsPage( ProperPageIdentity $page ): bool { |
270 | foreach ( $this->tasks as $task ) { |
271 | if ( Title::newFromLinkTarget( $task->getTitle() )->isSamePageAs( $page ) ) { |
272 | return true; |
273 | } |
274 | } |
275 | return false; |
276 | } |
277 | |
278 | } |