Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
68.42% covered (warning)
68.42%
39 / 57
15.79% covered (danger)
15.79%
3 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
LinkRecommendationTaskType
68.42% covered (warning)
68.42%
39 / 57
15.79% covered (danger)
15.79%
3 / 19
30.37
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 getMinimumTasksPerTopic
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMinimumLinksPerTask
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMinimumLinkScore
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMaximumLinksPerTask
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMaximumLinksToShowPerTask
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMinimumTimeSinceLastEdit
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMinimumWordCount
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMaximumWordCount
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMaxTasksPerDay
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getExcludedSections
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUnderlinkedWeight
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUnderlinkedMinLength
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 shouldOpenInEditMode
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDefaultEditSection
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getQualityGateIds
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getViewData
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 toJsonArray
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
1
 newFromJsonArray
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace GrowthExperiments\NewcomerTasks\TaskType;
4
5use MediaWiki\Json\JsonUnserializer;
6use MessageLocalizer;
7use Wikimedia\LightweightObjectStore\ExpirationAwareness;
8
9class LinkRecommendationTaskType extends TaskType {
10
11    /** @see ::getMinimumTasksPerTopic */
12    public const FIELD_MIN_TASKS_PER_TOPIC = 'minimumTasksPerTopic';
13    /** @see :getMinimumLinksPerTask */
14    public const FIELD_MIN_LINKS_PER_TASK = 'minimumLinksPerTask';
15    /** @see :getMinimumLinkScore */
16    public const FIELD_MIN_LINK_SCORE = 'minimumLinkScore';
17    /** @see :getMaximumLinksPerTask */
18    public const FIELD_MAX_LINKS_PER_TASK = 'maximumLinksPerTask';
19    /** @see :getMaximumLinksToShowPerTask */
20    public const FIELD_MAX_LINKS_TO_SHOW_PER_TASK = 'maximumLinksToShowPerTask';
21    /** @see :getMinimumTimeSinceLastEdit */
22    public const FIELD_MIN_TIME_SINCE_LAST_EDIT = 'minimumTimeSinceLastEdit';
23    /** @see :getMinimumWordCount */
24    public const FIELD_MIN_WORD_COUNT = 'minimumWordCount';
25    /** @see :getMaximumWordCount */
26    public const FIELD_MAX_WORD_COUNT = 'maximumWordCount';
27    /** @see :getMaxTasksPerDay */
28    public const FIELD_MAX_TASKS_PER_DAY = 'maxTasksPerDay';
29    /** @see :getExcludedSections */
30    public const FIELD_EXCLUDED_SECTIONS = 'excludedSections';
31    /** @see :getUnderlinkedWeight */
32    public const FIELD_UNDERLINKED_WEIGHT = 'underlinkedWeight';
33    /** @see :getUnderlinkedMinLength */
34    public const FIELD_UNDERLINKED_MIN_LENGTH = 'underlinkedMinLength';
35
36    /** Exclude a (task page, target page) pair from future tasks after this many rejections. */
37    public const REJECTION_EXCLUSION_LIMIT = 2;
38
39    public const DEFAULT_SETTINGS = [
40        self::FIELD_MIN_TASKS_PER_TOPIC => 500,
41        self::FIELD_MIN_LINKS_PER_TASK => 2,
42        self::FIELD_MIN_LINK_SCORE => 0.6,
43        self::FIELD_MAX_LINKS_PER_TASK => 10,
44        self::FIELD_MAX_LINKS_TO_SHOW_PER_TASK => 3,
45        self::FIELD_MIN_TIME_SINCE_LAST_EDIT => ExpirationAwareness::TTL_DAY,
46        self::FIELD_MIN_WORD_COUNT => 0,
47        self::FIELD_MAX_WORD_COUNT => PHP_INT_MAX,
48        self::FIELD_MAX_TASKS_PER_DAY => 25,
49        self::FIELD_EXCLUDED_SECTIONS => [],
50        self::FIELD_UNDERLINKED_WEIGHT => 0.5,
51        self::FIELD_UNDERLINKED_MIN_LENGTH => 300,
52    ];
53
54    /** @inheritDoc */
55    protected const IS_MACHINE_SUGGESTION = true;
56
57    /** @var int */
58    protected $minimumTasksPerTopic;
59    /** @var int */
60    protected $minimumLinksPerTask;
61    /** @var float */
62    protected $minimumLinkScore;
63    /** @var int */
64    protected $maximumLinksPerTask;
65    /** @var int */
66    protected $maximumLinksToShowPerTask;
67    /** @var int */
68    protected $minimumTimeSinceLastEdit;
69    /** @var int */
70    protected $minimumWordCount;
71    /** @var int */
72    protected $maximumWordCount;
73    /** @var int */
74    protected $maxTasksPerDay;
75    /** @var string[] */
76    protected $excludedSections;
77    /** @var float */
78    protected $underlinkedWeight;
79    /** @var int */
80    protected $underlinkedMinLength;
81
82    /**
83     * @inheritDoc
84     * @param array $settings A settings array matching LinkRecommendationTaskType::DEFAULT_SETTINGS.
85     */
86    public function __construct(
87        $id,
88        $difficulty,
89        array $settings = [],
90        array $extraData = [],
91        array $excludedTemplates = [],
92        array $excludedCategories = []
93    ) {
94        parent::__construct( $id, $difficulty, $extraData, $excludedTemplates, $excludedCategories );
95        $settings += self::DEFAULT_SETTINGS;
96        $this->minimumTasksPerTopic = $settings[self::FIELD_MIN_TASKS_PER_TOPIC];
97        $this->minimumLinksPerTask = $settings[self::FIELD_MIN_LINKS_PER_TASK];
98        $this->minimumLinkScore = $settings[self::FIELD_MIN_LINK_SCORE];
99        $this->maximumLinksPerTask = $settings[self::FIELD_MAX_LINKS_PER_TASK];
100        $this->maximumLinksToShowPerTask = $settings[self::FIELD_MAX_LINKS_TO_SHOW_PER_TASK];
101        $this->minimumTimeSinceLastEdit = $settings[self::FIELD_MIN_TIME_SINCE_LAST_EDIT];
102        $this->minimumWordCount = $settings[self::FIELD_MIN_WORD_COUNT];
103        $this->maximumWordCount = $settings[self::FIELD_MAX_WORD_COUNT];
104        $this->maxTasksPerDay = $settings[self::FIELD_MAX_TASKS_PER_DAY];
105        $this->excludedSections = $settings[self::FIELD_EXCLUDED_SECTIONS];
106        $this->underlinkedWeight = $settings[self::FIELD_UNDERLINKED_WEIGHT];
107        $this->underlinkedMinLength = $settings[self::FIELD_UNDERLINKED_MIN_LENGTH];
108    }
109
110    /**
111     * Try to have at least this many link recommendations prepared for each ORES topic.
112     * Recommendations are filled up every hour to this level.
113     * Note: these are individual ORES topics, not the combined Growth topics defined via
114     * $wgGENewcomerTasksOresTopicConfigTitle.
115     * @return int
116     */
117    public function getMinimumTasksPerTopic(): int {
118        return $this->minimumTasksPerTopic;
119    }
120
121    /**
122     * Each recommendation must contain at least this many suggested links.
123     * @return int
124     */
125    public function getMinimumLinksPerTask(): int {
126        return $this->minimumLinksPerTask;
127    }
128
129    /**
130     * Required confidence score for each link.
131     * @return float
132     */
133    public function getMinimumLinkScore(): float {
134        return $this->minimumLinkScore;
135    }
136
137    /**
138     * The maximum number of links that the refreshLinkRecommendations maintenance script will
139     * request when calling the link recommendation service for an article.
140     *
141     * @see ServiceLinkRecommendationProvider::get()
142     * @return int
143     */
144    public function getMaximumLinksPerTask(): int {
145        return $this->maximumLinksPerTask;
146    }
147
148    /**
149     * The maximum number of link recommendations that will be shown in the AddLink plugin to VisualEditor.
150     * @see AddLinkArticleTarget.js#annotateSuggestions
151     * @return int
152     */
153    public function getMaximumLinksToShowPerTask(): int {
154        return (int)$this->maximumLinksToShowPerTask;
155    }
156
157    /**
158     * At least this much time (in seconds) needs to have passed since the article was last edited.
159     * @return int
160     */
161    public function getMinimumTimeSinceLastEdit(): int {
162        return $this->minimumTimeSinceLastEdit;
163    }
164
165    /**
166     * Only use articles with this at least many words for link recommendations.
167     * (The word count will be some naive wikitext-based estimation.)
168     * @return int
169     */
170    public function getMinimumWordCount(): int {
171        return $this->minimumWordCount;
172    }
173
174    /**
175     * Only use articles with this at most many words for link recommendations.
176     * (The word count will be some naive wikitext-based estimation.)
177     * @return int
178     */
179    public function getMaximumWordCount(): int {
180        return $this->maximumWordCount;
181    }
182
183    /**
184     * The maximum number of image recommendation tasks that a user can perform each calendar day.
185     *
186     * @return int
187     */
188    public function getMaxTasksPerDay(): int {
189        return $this->maxTasksPerDay;
190    }
191
192    /**
193     * The list of sections which should be excluded when recommending links.
194     *
195     * @return string[]
196     */
197    public function getExcludedSections(): array {
198        return $this->excludedSections;
199    }
200
201    /**
202     * Weight of the underlinkedness metric (vs. a random factor) in sorting.
203     * Higher is less random. E.g. a weight of 0.25 means the scoring function will be
204     * 0.25 * <underlinkedness> + 0.75 * random(0,1).
205     * @return float
206     */
207    public function getUnderlinkedWeight(): float {
208        return $this->underlinkedWeight;
209    }
210
211    /**
212     * Minimum length above which an article can be considered underlinked.
213     * If the article size is smaller than this, its underlinkedness score will be 0.
214     * FIXME is this useful given that we already have a min words limit?
215     * @return int
216     */
217    public function getUnderlinkedMinLength(): int {
218        return $this->underlinkedMinLength;
219    }
220
221    /** @inheritDoc */
222    public function shouldOpenInEditMode(): bool {
223        return true;
224    }
225
226    /** @inheritDoc */
227    public function getDefaultEditSection(): string {
228        return 'all';
229    }
230
231    /** @inheritDoc */
232    public function getQualityGateIds(): array {
233        return [ 'dailyLimit' ];
234    }
235
236    /** @inheritDoc */
237    public function getViewData( MessageLocalizer $messageLocalizer ): array {
238        return parent::getViewData( $messageLocalizer ) + [
239            self::FIELD_MAX_LINKS_TO_SHOW_PER_TASK => $this->getMaximumLinksToShowPerTask()
240        ];
241    }
242
243    /** @inheritDoc */
244    protected function toJsonArray(): array {
245        return parent::toJsonArray() + [
246                'settings' => [
247                    'minimumTasksPerTopic' => $this->minimumTasksPerTopic,
248                    'minimumLinksPerTask' => $this->minimumLinksPerTask,
249                    'minimumLinkScore' => $this->minimumLinkScore,
250                    'maximumLinksPerTask' => $this->maximumLinksPerTask,
251                    'maximumLinksToShowPerTask' => $this->maximumLinksToShowPerTask,
252                    'minimumTimeSinceLastEdit' => $this->minimumTimeSinceLastEdit,
253                    'minimumWordCount' => $this->minimumWordCount,
254                    'maximumWordCount' => $this->maximumWordCount,
255                    'maxTasksPerDay' => $this->maxTasksPerDay,
256                    'underlinkedWeight' => $this->underlinkedWeight,
257                    'underlinkedMinLength' => $this->underlinkedMinLength,
258                ],
259            ];
260    }
261
262    /** @inheritDoc */
263    public static function newFromJsonArray( JsonUnserializer $unserializer, array $json ) {
264        $taskType = new LinkRecommendationTaskType(
265            $json['id'],
266            $json['difficulty'],
267            $json['settings'],
268            $json['extraData'],
269            self::getExcludedTemplatesTitleValues( $json ),
270            self::getExcludedCategoriesTitleValues( $json )
271        );
272        $taskType->setHandlerId( $json['handlerId'] );
273        return $taskType;
274    }
275
276}