Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
68.42% |
39 / 57 |
|
15.79% |
3 / 19 |
CRAP | |
0.00% |
0 / 1 |
LinkRecommendationTaskType | |
68.42% |
39 / 57 |
|
15.79% |
3 / 19 |
30.37 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
1 | |||
getMinimumTasksPerTopic | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getMinimumLinksPerTask | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getMinimumLinkScore | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getMaximumLinksPerTask | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getMaximumLinksToShowPerTask | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getMinimumTimeSinceLastEdit | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getMinimumWordCount | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getMaximumWordCount | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getMaxTasksPerDay | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getExcludedSections | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getUnderlinkedWeight | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getUnderlinkedMinLength | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
shouldOpenInEditMode | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDefaultEditSection | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getQualityGateIds | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getViewData | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
toJsonArray | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
1 | |||
newFromJsonArray | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | namespace GrowthExperiments\NewcomerTasks\TaskType; |
4 | |
5 | use MediaWiki\Json\JsonUnserializer; |
6 | use MessageLocalizer; |
7 | use Wikimedia\LightweightObjectStore\ExpirationAwareness; |
8 | |
9 | class 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 | } |