Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
78.65% |
70 / 89 |
|
52.94% |
9 / 17 |
CRAP | |
0.00% |
0 / 1 |
UserImpact | |
78.65% |
70 / 89 |
|
52.94% |
9 / 17 |
26.71 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
1 | |||
getUser | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getReceivedThanksCount | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getEditCountByNamespace | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getEditCountIn | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getEditCountByDay | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getNewcomerTaskEditCount | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getEditCountByTaskType | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getRevertedEditCount | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getLastEditTimestamp | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getGeneratedAt | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTotalEditsCount | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getLongestEditingStreakCount | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
newEmpty | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
1 | |||
newFromJsonArray | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
3.03 | |||
loadFromJsonArray | |
66.67% |
16 / 24 |
|
0.00% |
0 / 1 |
3.33 | |||
jsonSerialize | |
78.26% |
18 / 23 |
|
0.00% |
0 / 1 |
2.04 |
1 | <?php |
2 | |
3 | namespace GrowthExperiments\UserImpact; |
4 | |
5 | use JsonSerializable; |
6 | use LogicException; |
7 | use MediaWiki\User\UserIdentity; |
8 | use MediaWiki\User\UserIdentityValue; |
9 | use Wikimedia\Assert\Assert; |
10 | use Wikimedia\Assert\ParameterAssertionException; |
11 | use Wikimedia\Timestamp\ConvertibleTimestamp; |
12 | |
13 | /** |
14 | * Value object representing a user's impact statistics. |
15 | * This is generated data pieced together from contributions, thanks etc. |
16 | * This is information relevant for new users, and not always realistic to include data |
17 | * from arbitrarily long ago, so the data might use cutoffs that wouldn't affect a recently |
18 | * registered user with a limited number of edits. |
19 | */ |
20 | class UserImpact implements JsonSerializable { |
21 | |
22 | /** Cache version, to be increased when breaking backwards compatibility. */ |
23 | public const VERSION = 10; |
24 | |
25 | /** @var UserIdentity */ |
26 | private $user; |
27 | |
28 | /** @var int */ |
29 | private $receivedThanksCount; |
30 | |
31 | /** @var int[] */ |
32 | private $editCountByNamespace; |
33 | |
34 | /** @var int[] */ |
35 | private $editCountByDay; |
36 | |
37 | private int $revertedEditCount; |
38 | |
39 | /** @var int */ |
40 | private $newcomerTaskEditCount; |
41 | |
42 | /** @var int|null */ |
43 | private $lastEditTimestamp; |
44 | |
45 | /** @var int */ |
46 | private $generatedAt; |
47 | |
48 | /** @var EditingStreak */ |
49 | private EditingStreak $longestEditingStreak; |
50 | private array $editCountByTaskType; |
51 | |
52 | /** @var int|null Copy of user.user_editcount */ |
53 | private ?int $totalUserEditCount; |
54 | |
55 | /** |
56 | * @param UserIdentity $user |
57 | * @param int $receivedThanksCount Number of thanks the user has received. Might exclude |
58 | * thanks received a long time ago. |
59 | * @param int[] $editCountByNamespace Namespace ID => number of edits the user made in some |
60 | * namespace. Might exclude edits made a long time ago or many edits ago. |
61 | * @param int[] $editCountByDay Day => number of edits the user made on that day. Indexed with |
62 | * ISO 8601 dates, e.g. '2022-08-25'. Might exclude edits made many edits ago. |
63 | * @param array $editCountByTaskType |
64 | * @param int $revertedEditCount Number of edits by the user that got reverted (determined by |
65 | * the mw-reverted tag). |
66 | * @param int $newcomerTaskEditCount Number of edits the user made which have the |
67 | * newcomer task tag. Might exclude edits made a long time ago or many edits ago. |
68 | * @param int|null $lastEditTimestamp Unix timestamp of the user's last edit. |
69 | * @param EditingStreak $longestEditingStreak |
70 | * @param int|null $totalUserEditCount Copy of user.user_editcount for the user |
71 | */ |
72 | public function __construct( |
73 | UserIdentity $user, |
74 | int $receivedThanksCount, |
75 | array $editCountByNamespace, |
76 | array $editCountByDay, |
77 | array $editCountByTaskType, |
78 | int $revertedEditCount, |
79 | int $newcomerTaskEditCount, |
80 | ?int $lastEditTimestamp, |
81 | EditingStreak $longestEditingStreak, |
82 | ?int $totalUserEditCount |
83 | ) { |
84 | $this->user = $user; |
85 | $this->receivedThanksCount = $receivedThanksCount; |
86 | $this->editCountByNamespace = $editCountByNamespace; |
87 | $this->editCountByDay = $editCountByDay; |
88 | $this->editCountByTaskType = $editCountByTaskType; |
89 | $this->revertedEditCount = $revertedEditCount; |
90 | $this->newcomerTaskEditCount = $newcomerTaskEditCount; |
91 | $this->lastEditTimestamp = $lastEditTimestamp; |
92 | $this->generatedAt = ConvertibleTimestamp::time(); |
93 | $this->longestEditingStreak = $longestEditingStreak; |
94 | $this->totalUserEditCount = $totalUserEditCount; |
95 | } |
96 | |
97 | /** |
98 | * Get the user whose impact this object represents. |
99 | * @return UserIdentity |
100 | */ |
101 | public function getUser() { |
102 | return $this->user; |
103 | } |
104 | |
105 | /** |
106 | * Number of thanks the user has received. |
107 | * Might exclude thanks received a long time ago. |
108 | * @return int |
109 | */ |
110 | public function getReceivedThanksCount(): int { |
111 | return $this->receivedThanksCount; |
112 | } |
113 | |
114 | /** |
115 | * Map of namespace ID => number of edits the user made in that namespace. |
116 | * Might exclude edits made a long time ago or many edits ago. |
117 | * @return int[] |
118 | */ |
119 | public function getEditCountByNamespace(): array { |
120 | return $this->editCountByNamespace; |
121 | } |
122 | |
123 | /** |
124 | * Number of edits the user made in the given namespace. |
125 | * Might exclude edits made a long time ago or many edits ago. |
126 | * @param int $namespace |
127 | * @return int |
128 | */ |
129 | public function getEditCountIn( int $namespace ): int { |
130 | return $this->editCountByNamespace[$namespace] ?? 0; |
131 | } |
132 | |
133 | /** |
134 | * Map of day => number of article-space edits the user made on that day. |
135 | * Indexed with ISO 8601 dates, e.g. '2022-08-25'; in ascending order by date. |
136 | * Dates aren't contiguous. Might exclude edits made many edits ago. |
137 | * @return int[] |
138 | */ |
139 | public function getEditCountByDay(): array { |
140 | return $this->editCountByDay; |
141 | } |
142 | |
143 | /** |
144 | * Number of edits the user made which have the newcomer task tag. |
145 | * Might exclude edits made a long time ago or many edits ago. |
146 | * @return int |
147 | */ |
148 | public function getNewcomerTaskEditCount(): int { |
149 | return $this->newcomerTaskEditCount; |
150 | } |
151 | |
152 | /** |
153 | * Number of newcomer task edits the user has made for each task type. |
154 | * |
155 | * @return array<string,int> (task type id => edit count for the task type) |
156 | */ |
157 | public function getEditCountByTaskType(): array { |
158 | return $this->editCountByTaskType; |
159 | } |
160 | |
161 | /** |
162 | * Number of total edits by the user that got reverted. |
163 | * @return int |
164 | */ |
165 | public function getRevertedEditCount(): int { |
166 | return $this->revertedEditCount; |
167 | } |
168 | |
169 | /** |
170 | * Unix timestamp of the user's last edit, or null if the user has zero edits. |
171 | * @return int|null |
172 | */ |
173 | public function getLastEditTimestamp(): ?int { |
174 | return $this->lastEditTimestamp; |
175 | } |
176 | |
177 | /** |
178 | * Unix timestamp of when the user impact data was generated. |
179 | * @return int |
180 | */ |
181 | public function getGeneratedAt(): int { |
182 | return $this->generatedAt; |
183 | } |
184 | |
185 | /** |
186 | * Total number of edits across all namespaces. |
187 | * |
188 | * @note Unlike all other methods in this class, this one is not capped to recent edits (in |
189 | * other words, ComputedUserImpactLookup::MAX_EDITS is ignored). |
190 | * @return int |
191 | */ |
192 | public function getTotalEditsCount(): int { |
193 | return $this->totalUserEditCount ?? array_sum( $this->editCountByNamespace ); |
194 | } |
195 | |
196 | /** |
197 | * Total number of edits for the user's longest editing streak. |
198 | * |
199 | * @return int Number of edits the user had in their longest editing streak |
200 | */ |
201 | public function getLongestEditingStreakCount(): int { |
202 | return $this->longestEditingStreak->getTotalEditCountForPeriod(); |
203 | } |
204 | |
205 | /** |
206 | * Helper method for newFromJsonArray. |
207 | * @return UserImpact |
208 | */ |
209 | protected static function newEmpty(): self { |
210 | return new UserImpact( |
211 | new UserIdentityValue( 0, '' ), |
212 | 0, |
213 | [], |
214 | [], |
215 | [], |
216 | 0, |
217 | 0, |
218 | 0, |
219 | new EditingStreak(), |
220 | 0 |
221 | ); |
222 | } |
223 | |
224 | /** |
225 | * @param array $json |
226 | * @return UserImpact |
227 | * @throws ParameterAssertionException when trying to load an incompatible old JSON format. |
228 | */ |
229 | public static function newFromJsonArray( array $json ): UserImpact { |
230 | if ( array_key_exists( 'dailyTotalViews', $json ) ) { |
231 | $userImpact = ExpensiveUserImpact::newEmpty(); |
232 | } elseif ( array_key_exists( 'topViewedArticles', $json ) ) { |
233 | // UserImpactFormatter::jsonSerialize() unsets the 'dailyArticleViews' |
234 | // field so deserializing it would be tricky, but it's not needed anyway. |
235 | throw new LogicException( 'UserImpactFormatter is not deserializable.' ); |
236 | } else { |
237 | $userImpact = self::newEmpty(); |
238 | } |
239 | $userImpact->loadFromJsonArray( $json ); |
240 | return $userImpact; |
241 | } |
242 | |
243 | /** |
244 | * @param array $json |
245 | * @throws ParameterAssertionException when trying to load an incompatible old JSON format. |
246 | */ |
247 | protected function loadFromJsonArray( array $json ): void { |
248 | if ( $json['@version'] !== self::VERSION ) { |
249 | throw new ParameterAssertionException( '@version', 'must be ' . self::VERSION ); |
250 | } |
251 | |
252 | Assert::parameterKeyType( 'integer', $json['editCountByNamespace'], '$json[\'editCountByNamespace\']' ); |
253 | Assert::parameterElementType( 'integer', $json['editCountByNamespace'], '$json[\'editCountByNamespace\']' ); |
254 | Assert::parameterKeyType( 'string', $json['editCountByDay'], '$json[\'editCountByDay\']' ); |
255 | Assert::parameterElementType( 'integer', $json['editCountByDay'], '$json[\'editCountByDay\']' ); |
256 | |
257 | $this->user = UserIdentityValue::newRegistered( $json['userId'], $json['userName'] ); |
258 | $this->receivedThanksCount = $json['receivedThanksCount']; |
259 | $this->editCountByNamespace = $json['editCountByNamespace']; |
260 | $this->editCountByDay = $json['editCountByDay']; |
261 | $this->editCountByTaskType = $json['editCountByTaskType']; |
262 | $this->totalUserEditCount = $json['totalUserEditCount']; |
263 | $this->revertedEditCount = $json['revertedEditCount']; |
264 | $this->newcomerTaskEditCount = $json['newcomerTaskEditCount']; |
265 | $this->lastEditTimestamp = $json['lastEditTimestamp']; |
266 | $this->generatedAt = $json['generatedAt']; |
267 | $this->longestEditingStreak = $json['longestEditingStreak'] === '' ? new EditingStreak() : |
268 | new EditingStreak( |
269 | ComputeEditingStreaks::makeDatePeriod( |
270 | $json['longestEditingStreak']['datePeriod']['start'], |
271 | $json['longestEditingStreak']['datePeriod']['end'] |
272 | ), |
273 | $json['longestEditingStreak']['totalEditCountForPeriod'] |
274 | ); |
275 | } |
276 | |
277 | /** @inheritDoc */ |
278 | public function jsonSerialize(): array { |
279 | $longestEditingStreak = $this->longestEditingStreak->getDatePeriod() ? |
280 | [ 'datePeriod' => [ |
281 | 'start' => $this->longestEditingStreak->getDatePeriod()->getStartDate()->format( 'Y-m-d' ), |
282 | 'end' => $this->longestEditingStreak->getDatePeriod()->getEndDate()->format( 'Y-m-d' ), |
283 | 'days' => $this->longestEditingStreak->getStreakNumberOfDays() |
284 | ], 'totalEditCountForPeriod' => $this->longestEditingStreak->getTotalEditCountForPeriod() ] : |
285 | ''; |
286 | return [ |
287 | '@version' => self::VERSION, |
288 | 'userId' => $this->user->getId(), |
289 | 'userName' => $this->user->getName(), |
290 | 'receivedThanksCount' => $this->receivedThanksCount, |
291 | 'editCountByNamespace' => $this->editCountByNamespace, |
292 | 'editCountByDay' => $this->editCountByDay, |
293 | 'editCountByTaskType' => $this->editCountByTaskType, |
294 | 'totalUserEditCount' => $this->totalUserEditCount, |
295 | 'revertedEditCount' => $this->revertedEditCount, |
296 | 'newcomerTaskEditCount' => $this->newcomerTaskEditCount, |
297 | 'lastEditTimestamp' => $this->lastEditTimestamp, |
298 | 'generatedAt' => $this->generatedAt, |
299 | 'longestEditingStreak' => $longestEditingStreak, |
300 | 'totalEditsCount' => $this->getTotalEditsCount() |
301 | ]; |
302 | } |
303 | |
304 | } |