Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
78.65% covered (warning)
78.65%
70 / 89
52.94% covered (warning)
52.94%
9 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
UserImpact
78.65% covered (warning)
78.65%
70 / 89
52.94% covered (warning)
52.94%
9 / 17
26.71
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 getUser
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getReceivedThanksCount
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getEditCountByNamespace
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getEditCountIn
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getEditCountByDay
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getNewcomerTaskEditCount
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getEditCountByTaskType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRevertedEditCount
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLastEditTimestamp
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getGeneratedAt
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTotalEditsCount
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLongestEditingStreakCount
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 newEmpty
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
 newFromJsonArray
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 loadFromJsonArray
66.67% covered (warning)
66.67%
16 / 24
0.00% covered (danger)
0.00%
0 / 1
3.33
 jsonSerialize
78.26% covered (warning)
78.26%
18 / 23
0.00% covered (danger)
0.00%
0 / 1
2.04
1<?php
2
3namespace GrowthExperiments\UserImpact;
4
5use JsonSerializable;
6use LogicException;
7use MediaWiki\User\UserIdentity;
8use MediaWiki\User\UserIdentityValue;
9use Wikimedia\Assert\Assert;
10use Wikimedia\Assert\ParameterAssertionException;
11use 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 */
20class 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}