Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
45.65% |
21 / 46 |
|
28.57% |
2 / 7 |
CRAP | |
0.00% |
0 / 1 |
PraiseworthyConditionsLookup | |
45.65% |
21 / 46 |
|
28.57% |
2 / 7 |
70.01 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
buildDatePeriod | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
getEditsInDatePeriod | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
wasMenteePraised | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
isMenteeSkipped | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
canUserBePraised | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
42 | |||
isMenteePraiseworthyForMentor | |
90.00% |
9 / 10 |
|
0.00% |
0 / 1 |
5.03 |
1 | <?php |
2 | |
3 | namespace GrowthExperiments\MentorDashboard\PersonalizedPraise; |
4 | |
5 | use DateInterval; |
6 | use DatePeriod; |
7 | use DateTime; |
8 | use GrowthExperiments\HomepageHooks; |
9 | use GrowthExperiments\Mentorship\MentorManager; |
10 | use GrowthExperiments\UserImpact\UserImpact; |
11 | use MediaWiki\User\Options\UserOptionsLookup; |
12 | use MediaWiki\User\UserFactory; |
13 | use MediaWiki\User\UserIdentity; |
14 | use Wikimedia\Timestamp\ConvertibleTimestamp; |
15 | |
16 | /** |
17 | * Service to look up the conditions for mentee being praiseworthy |
18 | */ |
19 | class PraiseworthyConditionsLookup { |
20 | |
21 | private PersonalizedPraiseSettings $settings; |
22 | private UserOptionsLookup $userOptionsLookup; |
23 | private UserFactory $userFactory; |
24 | private MentorManager $mentorManager; |
25 | |
26 | /** @var string */ |
27 | public const WAS_PRAISED_PREF = 'growthexperiments-mentorship-was-praised'; |
28 | |
29 | /** @var string */ |
30 | public const SKIPPED_UNTIL_PREF = 'growthexperiments-personalized-praise-skipped-until'; |
31 | |
32 | /** @var int Number of days skipped mentees cannot be suggested for */ |
33 | public const SKIP_MENTEES_FOR_DAYS = 10; |
34 | |
35 | /** |
36 | * @param PersonalizedPraiseSettings $settings |
37 | * @param UserOptionsLookup $userOptionsLookup |
38 | * @param UserFactory $userFactory |
39 | * @param MentorManager $mentorManager |
40 | */ |
41 | public function __construct( |
42 | PersonalizedPraiseSettings $settings, |
43 | UserOptionsLookup $userOptionsLookup, |
44 | UserFactory $userFactory, |
45 | MentorManager $mentorManager |
46 | ) { |
47 | $this->settings = $settings; |
48 | $this->userOptionsLookup = $userOptionsLookup; |
49 | $this->userFactory = $userFactory; |
50 | $this->mentorManager = $mentorManager; |
51 | } |
52 | |
53 | /** |
54 | * Build a DatePeriod going from today to today minus $days days |
55 | * |
56 | * @param int $days |
57 | * @return DatePeriod |
58 | */ |
59 | private function buildDatePeriod( int $days ): DatePeriod { |
60 | $daysAgoUnix = ( new ConvertibleTimestamp() )->sub( 'P' . $days . 'D' )->getTimestamp( TS_UNIX ); |
61 | $tomorrowUnix = ( new ConvertibleTimestamp() )->add( 'P1D' )->getTimestamp( TS_UNIX ); |
62 | return new DatePeriod( |
63 | new DateTime( '@' . $daysAgoUnix ), |
64 | new DateInterval( 'P1D' ), |
65 | new DateTime( '@' . $tomorrowUnix ) |
66 | ); |
67 | } |
68 | |
69 | /** |
70 | * Find how many edits the mentee made in a DatePeriod |
71 | * |
72 | * @param UserImpact $menteeImpact |
73 | * @param DatePeriod $datePeriod |
74 | * @return int |
75 | */ |
76 | private function getEditsInDatePeriod( |
77 | UserImpact $menteeImpact, |
78 | DatePeriod $datePeriod |
79 | ): int { |
80 | $editCountByDay = $menteeImpact->getEditCountByDay(); |
81 | |
82 | $res = 0; |
83 | foreach ( $datePeriod as $day ) { |
84 | $res += $editCountByDay[$day->format( 'Y-m-d' )] ?? 0; |
85 | } |
86 | |
87 | return $res; |
88 | } |
89 | |
90 | /** |
91 | * Was the mentee ever praised by their mentor? |
92 | * |
93 | * @param UserIdentity $mentee |
94 | * @return bool |
95 | */ |
96 | private function wasMenteePraised( UserIdentity $mentee ): bool { |
97 | return $this->userOptionsLookup->getBoolOption( |
98 | $mentee, |
99 | self::WAS_PRAISED_PREF |
100 | ); |
101 | } |
102 | |
103 | /** |
104 | * Is the mentee currently skipped? |
105 | * |
106 | * @param UserIdentity $mentee |
107 | * @return bool |
108 | */ |
109 | private function isMenteeSkipped( UserIdentity $mentee ): bool { |
110 | $skippedUntilRaw = $this->userOptionsLookup->getOption( |
111 | $mentee, |
112 | self::SKIPPED_UNTIL_PREF |
113 | ); |
114 | if ( $skippedUntilRaw === null ) { |
115 | return false; |
116 | } |
117 | |
118 | $noPraiseUntilUnix = ( new ConvertibleTimestamp( $skippedUntilRaw ) )->getTimestamp( TS_UNIX ); |
119 | $nowUnix = ( new ConvertibleTimestamp() )->getTimestamp( TS_UNIX ); |
120 | return $nowUnix < $noPraiseUntilUnix; |
121 | } |
122 | |
123 | /** |
124 | * Is the user able to be praised by a mentor? |
125 | * |
126 | * Users can receive praise if ALL of the following conditions are true: |
127 | * * has mentorship module enabled |
128 | * * was never praised in the past |
129 | * * either was not skipped at all or was skipped at least SKIP_MENTEES_FOR_DAYS days ago |
130 | * |
131 | * Use this method if you want to know whether an user can theoretically appear in the |
132 | * Personalized praise module. If you want to know whether they should be included in the |
133 | * module, call isMenteePraiseworthyForMentor instead. |
134 | * |
135 | * @param UserIdentity $mentee |
136 | * @return bool |
137 | */ |
138 | public function canUserBePraised( UserIdentity $mentee ): bool { |
139 | // NOTE: This does not use HomepageHooks::isHomepageEnabled, because it accesses the global |
140 | // state. Personalized praise is not directly tied to Homepage, so relying on the |
141 | // preference value alone is sufficient. |
142 | |
143 | $menteeUser = $this->userFactory->newFromUserIdentity( $mentee ); |
144 | return $this->userOptionsLookup->getBoolOption( $mentee, HomepageHooks::HOMEPAGE_PREF_ENABLE ) && |
145 | $this->mentorManager->getMentorshipStateForUser( $mentee ) === MentorManager::MENTORSHIP_ENABLED && |
146 | $menteeUser->isNamed() && |
147 | $menteeUser->getBlock() === null && |
148 | !$this->wasMenteePraised( $mentee ) && |
149 | !$this->isMenteeSkipped( $mentee ); |
150 | } |
151 | |
152 | /** |
153 | * Is the mentee praiseworthy for a given mentor? |
154 | * |
155 | * @param UserImpact $menteeImpact |
156 | * @param UserIdentity $mentor |
157 | * @return bool |
158 | */ |
159 | public function isMenteePraiseworthyForMentor( |
160 | UserImpact $menteeImpact, |
161 | UserIdentity $mentor |
162 | ): bool { |
163 | if ( !$this->canUserBePraised( $menteeImpact->getUser() ) ) { |
164 | return false; |
165 | } |
166 | |
167 | $conditions = $this->settings->getPraiseworthyConditions( $mentor ); |
168 | |
169 | if ( $menteeImpact->getTotalEditsCount() >= $conditions->getMaxEdits() ) { |
170 | return false; |
171 | } |
172 | if ( |
173 | $conditions->getMaxReverts() !== null && |
174 | $menteeImpact->getRevertedEditCount() > $conditions->getMaxReverts() ) { |
175 | return false; |
176 | } |
177 | |
178 | $datePeriod = $this->buildDatePeriod( $conditions->getDays() ); |
179 | return $this->getEditsInDatePeriod( $menteeImpact, $datePeriod ) >= $conditions->getMinEdits(); |
180 | } |
181 | } |