Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
52.54% covered (warning)
52.54%
31 / 59
22.73% covered (danger)
22.73%
5 / 22
CRAP
0.00% covered (danger)
0.00%
0 / 1
TaskSet
52.54% covered (warning)
52.54%
31 / 59
22.73% covered (danger)
22.73%
5 / 22
118.89
0.00% covered (danger)
0.00%
0 / 1
 __construct
16.67% covered (danger)
16.67%
1 / 6
0.00% covered (danger)
0.00%
0 / 1
1.58
 getIterator
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 count
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 offsetExists
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 offsetGet
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 offsetSet
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 offsetUnset
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTotalCount
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOffset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDebugData
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setDebugData
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getFilters
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 truncate
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 randomSort
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 filtersEqual
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getQualityGateConfig
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setQualityGateConfigForTaskType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setQualityGateConfig
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getInvalidTasks
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 toJsonArray
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
1
 newFromJsonArray
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
4
 containsPage
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace GrowthExperiments\NewcomerTasks\Task;
4
5use ArrayAccess;
6use ArrayIterator;
7use Countable;
8use IteratorAggregate;
9use LogicException;
10use MediaWiki\Json\JsonUnserializable;
11use MediaWiki\Json\JsonUnserializableTrait;
12use MediaWiki\Json\JsonUnserializer;
13use MediaWiki\Page\ProperPageIdentity;
14use MediaWiki\Title\Title;
15use OutOfBoundsException;
16use Traversable;
17use 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 */
24class 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}