Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
58.14% |
50 / 86 |
|
16.67% |
2 / 12 |
CRAP | |
0.00% |
0 / 1 |
TaskTypeHandler | |
58.14% |
50 / 86 |
|
16.67% |
2 / 12 |
131.06 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getId | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getSubmissionHandler | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
validateTaskTypeConfiguration | |
84.38% |
27 / 32 |
|
0.00% |
0 / 1 |
13.64 | |||
validateTemplate | |
60.00% |
6 / 10 |
|
0.00% |
0 / 1 |
5.02 | |||
validateCategory | |
60.00% |
6 / 10 |
|
0.00% |
0 / 1 |
5.02 | |||
validateTaskTypeObject | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
createTaskType | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getSearchTerm | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
createTaskFromSearchResult | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
getChangeTags | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTaskTypeIdByChangeTagName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
parseExcludedTemplates | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
parseExcludedCategories | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace GrowthExperiments\NewcomerTasks\TaskType; |
4 | |
5 | use GrowthExperiments\NewcomerTasks\ConfigurationLoader\ConfigurationValidator; |
6 | use GrowthExperiments\NewcomerTasks\SubmissionHandler; |
7 | use GrowthExperiments\NewcomerTasks\Task\Task; |
8 | use GrowthExperiments\NewcomerTasks\TaskSuggester\SearchStrategy\SearchQuery; |
9 | use MediaWiki\Linker\LinkTarget; |
10 | use MediaWiki\Title\MalformedTitleException; |
11 | use MediaWiki\Title\TitleParser; |
12 | use SearchResult; |
13 | use StatusValue; |
14 | |
15 | /** |
16 | * A TaskTypeHandler is responsible for all the type-specific behavior of some TaskType |
17 | * (or group of TaskTypes) such as constructing the TaskType object from configuration or |
18 | * constructing the search query that corresponds to the TaskType. |
19 | * |
20 | * TaskTypeHandlers are identified by their can be obtained from the TaskTypeRegis |
21 | */ |
22 | abstract class TaskTypeHandler { |
23 | |
24 | /** |
25 | * Change tag used to track edits made via suggested edit tasks. Subbtasks might add |
26 | * or replace with more specific tags. |
27 | */ |
28 | public const NEWCOMER_TASK_TAG = 'newcomer task'; |
29 | |
30 | /** @var ConfigurationValidator */ |
31 | protected $configurationValidator; |
32 | /** @var TitleParser */ |
33 | private $titleParser; |
34 | |
35 | /** |
36 | * @param ConfigurationValidator $configurationValidator |
37 | * @param TitleParser $titleParser |
38 | */ |
39 | public function __construct( ConfigurationValidator $configurationValidator, TitleParser $titleParser ) { |
40 | $this->configurationValidator = $configurationValidator; |
41 | $this->titleParser = $titleParser; |
42 | } |
43 | |
44 | /** |
45 | * Get the handler ID of this handler. |
46 | * This is mainly for internal use by TaskTypeHandlerRegistry. |
47 | * @return string |
48 | */ |
49 | abstract public function getId(): string; |
50 | |
51 | /** |
52 | * @return SubmissionHandler |
53 | */ |
54 | abstract public function getSubmissionHandler(): SubmissionHandler; |
55 | |
56 | /** |
57 | * Validate task configuration used by ConfigurationLoader. |
58 | * @param string $taskTypeId |
59 | * @param array $config |
60 | * @return StatusValue |
61 | * @see ::validateTaskTypeObject |
62 | */ |
63 | public function validateTaskTypeConfiguration( string $taskTypeId, array $config ): StatusValue { |
64 | $status = StatusValue::newGood(); |
65 | $status->merge( $this->configurationValidator->validateIdentifier( $taskTypeId ) ); |
66 | |
67 | $groupFieldStatus = $this->configurationValidator->validateRequiredField( 'group', |
68 | $config, $taskTypeId ); |
69 | $status->merge( $groupFieldStatus ); |
70 | if ( $groupFieldStatus->isOK() && |
71 | !in_array( $config['group'], TaskType::DIFFICULTY_CLASSES, true ) |
72 | ) { |
73 | $status->fatal( 'growthexperiments-homepage-suggestededits-config-invalidgroup', |
74 | $config['group'], $taskTypeId ); |
75 | } |
76 | |
77 | if ( $status->isOK() ) { |
78 | if ( !isset( $config['excludedTemplates'] ) ) { |
79 | $config['excludedTemplates'] = []; |
80 | } |
81 | $status->merge( |
82 | $this->configurationValidator->validateFieldIsArray( 'excludedTemplates', $config, $taskTypeId ) |
83 | ); |
84 | if ( $status->isOK() ) { |
85 | foreach ( $config['excludedTemplates'] as $template ) { |
86 | $this->validateTemplate( $template, $taskTypeId, $status ); |
87 | } |
88 | } |
89 | } |
90 | |
91 | if ( $status->isOK() ) { |
92 | if ( !isset( $config['excludedCategories'] ) ) { |
93 | $config['excludedCategories'] = []; |
94 | } |
95 | $status->merge( |
96 | $this->configurationValidator->validateFieldIsArray( 'excludedCategories', $config, $taskTypeId ) |
97 | ); |
98 | if ( $status->isOK() ) { |
99 | foreach ( $config['excludedCategories'] as $category ) { |
100 | $this->validateCategory( $category, $taskTypeId, $status ); |
101 | } |
102 | } |
103 | } |
104 | |
105 | // Link recommendation specific. |
106 | // FIXME: would be nice to define this and other type definitions in the task type. |
107 | if ( $status->isOK() && isset( $config['excludedSections'] ) ) { |
108 | $status->merge( |
109 | $this->configurationValidator->validateFieldIsArray( 'excludedSections', $config, $taskTypeId ) |
110 | ); |
111 | } |
112 | |
113 | return $status; |
114 | } |
115 | |
116 | /** |
117 | * Attempt to parse a template title, return a failed status value on MalformedTitleException. |
118 | * |
119 | * @param mixed $template |
120 | * @param string $taskTypeId |
121 | * @param StatusValue $status |
122 | * @return StatusValue |
123 | */ |
124 | protected function validateTemplate( $template, string $taskTypeId, StatusValue $status ): StatusValue { |
125 | if ( !is_string( $template ) ) { |
126 | if ( !is_scalar( $template ) ) { |
127 | $template = '[' . gettype( $template ) . ']'; |
128 | } |
129 | return $status->fatal( 'growthexperiments-homepage-suggestededits-config-invalidtemplatetitle', |
130 | $template, $taskTypeId ); |
131 | } |
132 | try { |
133 | $this->titleParser->parseTitle( $template, NS_TEMPLATE ); |
134 | } catch ( MalformedTitleException $e ) { |
135 | $status->fatal( 'growthexperiments-homepage-suggestededits-config-invalidtemplatetitle', |
136 | $template, $taskTypeId ); |
137 | } |
138 | return $status; |
139 | } |
140 | |
141 | /** |
142 | * Attempt to parse a category title, return a failed status value on MalformedTitleException. |
143 | * |
144 | * @param mixed $category |
145 | * @param string $taskTypeId |
146 | * @param StatusValue $status |
147 | * @return StatusValue |
148 | */ |
149 | protected function validateCategory( $category, string $taskTypeId, StatusValue $status ): StatusValue { |
150 | if ( !is_string( $category ) ) { |
151 | if ( !is_scalar( $category ) ) { |
152 | $category = '[' . gettype( $category ) . ']'; |
153 | } |
154 | return $status->fatal( 'growthexperiments-homepage-suggestededits-config-invalidcategorytitle', |
155 | $category, $taskTypeId ); |
156 | } |
157 | try { |
158 | $this->titleParser->parseTitle( $category, NS_CATEGORY ); |
159 | } catch ( MalformedTitleException $e ) { |
160 | $status->fatal( 'growthexperiments-homepage-suggestededits-config-invalidcategorytitle', |
161 | $category, $taskTypeId ); |
162 | } |
163 | return $status; |
164 | } |
165 | |
166 | /** |
167 | * Validate a task object. This is a companion to validateTaskTypeConfiguration() - some |
168 | * validation requires a TaskType object (typically checking whether messages exist) but |
169 | * first we need to make sure the configuration is valid enough to create the object, |
170 | * and the two cannot be done in the same method due to inheritance. |
171 | * @param TaskType $taskType |
172 | * @return StatusValue |
173 | */ |
174 | public function validateTaskTypeObject( TaskType $taskType ): StatusValue { |
175 | return $this->configurationValidator->validateTaskMessages( $taskType ); |
176 | } |
177 | |
178 | /** |
179 | * @param string $taskTypeId |
180 | * @param array $config Task type configuration. Caller is assumed to have checked it |
181 | * with validateTaskTypeConfiguration(). |
182 | * @return TaskType |
183 | * @note Subclasses overriding this method must set the handled ID of the task type. |
184 | */ |
185 | public function createTaskType( string $taskTypeId, array $config ): TaskType { |
186 | $extraData = [ 'learnMoreLink' => $config['learnmore'] ?? null ]; |
187 | $taskType = new TaskType( $taskTypeId, $config['group'], $extraData ); |
188 | $taskType->setHandlerId( $this->getId() ); |
189 | return $taskType; |
190 | } |
191 | |
192 | /** |
193 | * Get a CirrusSearch search term corresponding to this task. |
194 | * |
195 | * Task types extending this one must call this parent method to get exclusion search strings. |
196 | * |
197 | * @param TaskType $taskType |
198 | * @return string |
199 | */ |
200 | public function getSearchTerm( TaskType $taskType ): string { |
201 | $searchTerm = ''; |
202 | $excludedTemplates = $taskType->getExcludedTemplates(); |
203 | if ( $excludedTemplates ) { |
204 | // extra space added to facilitate concatenation |
205 | $searchTerm .= '-hastemplate:' . Util::escapeSearchTitleList( $excludedTemplates ) . ' '; |
206 | } |
207 | $excludedCategories = $taskType->getExcludedCategories(); |
208 | if ( $excludedCategories ) { |
209 | // extra space added to facilitate concatenation |
210 | $searchTerm .= '-incategory:' . Util::escapeSearchTitleList( $excludedCategories ) . ' '; |
211 | } |
212 | return $searchTerm; |
213 | } |
214 | |
215 | /** |
216 | * @param SearchQuery $query |
217 | * @param SearchResult $match |
218 | * @return Task |
219 | */ |
220 | public function createTaskFromSearchResult( SearchQuery $query, SearchResult $match ): Task { |
221 | $taskType = $query->getTaskType(); |
222 | $topic = $query->getTopic(); |
223 | $task = new Task( $taskType, $match->getTitle() ); |
224 | if ( $topic ) { |
225 | $score = 0; |
226 | // CirrusSearch and our custom FauxSearchResultWithScore have this. |
227 | if ( method_exists( $match, 'getScore' ) ) { |
228 | // @phan-suppress-next-line PhanUndeclaredMethod |
229 | $score = $match->getScore(); |
230 | } |
231 | $task->setTopics( [ $topic ], [ $topic->getId() => $score ] ); |
232 | } |
233 | |
234 | return $task; |
235 | } |
236 | |
237 | /** |
238 | * Get the list of change tags to apply to edits originating from this task type. |
239 | * @param string|null $taskType |
240 | * @return string[] |
241 | */ |
242 | public function getChangeTags( ?string $taskType = null ): array { |
243 | return [ self::NEWCOMER_TASK_TAG ]; |
244 | } |
245 | |
246 | /** |
247 | * Get the task type ID based on the change tag associated with it. |
248 | * |
249 | * @param string $changeTagName |
250 | * @return string|null |
251 | */ |
252 | public function getTaskTypeIdByChangeTagName( string $changeTagName ): ?string { |
253 | return null; |
254 | } |
255 | |
256 | /** |
257 | * @param array $config |
258 | * @return LinkTarget[] |
259 | * @throws MalformedTitleException |
260 | */ |
261 | protected function parseExcludedTemplates( array $config ): array { |
262 | $excludedTemplates = []; |
263 | foreach ( $config['excludedTemplates'] ?? [] as $excludedTemplate ) { |
264 | $excludedTemplates[] = $this->titleParser->parseTitle( $excludedTemplate, NS_TEMPLATE ); |
265 | } |
266 | return $excludedTemplates; |
267 | } |
268 | |
269 | /** |
270 | * @param array $config |
271 | * @return LinkTarget[] |
272 | * @throws MalformedTitleException |
273 | */ |
274 | protected function parseExcludedCategories( array $config ): array { |
275 | $excludedCategories = []; |
276 | foreach ( $config['excludedCategories'] ?? [] as $excludedCategory ) { |
277 | $excludedCategories[] = $this->titleParser->parseTitle( $excludedCategory, NS_CATEGORY ); |
278 | } |
279 | return $excludedCategories; |
280 | } |
281 | |
282 | } |