Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
22.45% covered (danger)
22.45%
11 / 49
10.00% covered (danger)
10.00%
1 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
PersonalizedPraiseSettings
22.45% covered (danger)
22.45%
11 / 49
10.00% covered (danger)
10.00%
1 / 10
105.42
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 loadSettings
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 toArray
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 castToNullableInt
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getPraiseworthyConditions
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 getPraisingMessageDefaultSubject
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getPraisingMessageUserTitle
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getPraisingMessageTitle
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getPraisingMessageContent
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getNotificationsFrequency
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace GrowthExperiments\MentorDashboard\PersonalizedPraise;
4
5use MediaWiki\Config\Config;
6use MediaWiki\Content\WikitextContent;
7use MediaWiki\Json\FormatJson;
8use MediaWiki\Revision\RevisionLookup;
9use MediaWiki\Revision\SlotRecord;
10use MediaWiki\Title\Title;
11use MediaWiki\Title\TitleFactory;
12use MediaWiki\User\Options\UserOptionsLookup;
13use MediaWiki\User\UserFactory;
14use MediaWiki\User\UserIdentity;
15use MessageLocalizer;
16
17/**
18 * Accessor for mentor's Personalized praise settings
19 *
20 * The settings are modified on the frontend (via action=options); this is why there are no
21 * setters available.
22 */
23class PersonalizedPraiseSettings {
24
25    public const NOTIFY_NEVER = -1;
26    public const NOTIFY_IMMEDIATELY = 0;
27
28    /** Note: This is hardcoded on the client side as well */
29    public const PREF_NAME = 'growthexperiments-personalized-praise-settings';
30    public const USER_MESSAGE_PRELOAD_SUBPAGE_NAME = 'Personalized praise message';
31
32    private const SETTING_MESSAGE_SUBJECT = 'messageSubject';
33    private const SETTING_MESSAGE_TEXT = 'messageText';
34    private const SETTING_NOTIFICATION_FREQUENCY = 'notificationFrequency';
35
36    private Config $wikiConfig;
37    private MessageLocalizer $messageLocalizer;
38    private UserOptionsLookup $userOptionsLookup;
39    private UserFactory $userFactory;
40    private TitleFactory $titleFactory;
41    private RevisionLookup $revisionLookup;
42
43    /**
44     * @param Config $wikiConfig
45     * @param MessageLocalizer $messageLocalizer
46     * @param UserOptionsLookup $userOptionsLookup
47     * @param UserFactory $userFactory
48     * @param TitleFactory $titleFactory
49     * @param RevisionLookup $revisionLookup
50     */
51    public function __construct(
52        Config $wikiConfig,
53        MessageLocalizer $messageLocalizer,
54        UserOptionsLookup $userOptionsLookup,
55        UserFactory $userFactory,
56        TitleFactory $titleFactory,
57        RevisionLookup $revisionLookup
58    ) {
59        $this->wikiConfig = $wikiConfig;
60        $this->messageLocalizer = $messageLocalizer;
61        $this->userOptionsLookup = $userOptionsLookup;
62        $this->userFactory = $userFactory;
63        $this->titleFactory = $titleFactory;
64        $this->revisionLookup = $revisionLookup;
65    }
66
67    /**
68     * @param UserIdentity $user
69     * @return array
70     */
71    private function loadSettings( UserIdentity $user ): array {
72        return FormatJson::decode( $this->userOptionsLookup->getOption(
73            $user, self::PREF_NAME
74        ), true ) ?? [];
75    }
76
77    /**
78     * @param UserIdentity $user
79     * @return array
80     */
81    public function toArray( UserIdentity $user ): array {
82        $conditions = $this->getPraiseworthyConditions( $user );
83        return array_merge( [
84            self::SETTING_MESSAGE_SUBJECT => $this->getPraisingMessageDefaultSubject( $user ),
85            self::SETTING_MESSAGE_TEXT => $this->getPraisingMessageContent( $user ),
86            self::SETTING_NOTIFICATION_FREQUENCY => $this->getNotificationsFrequency( $user ),
87        ], $conditions->jsonSerialize() );
88    }
89
90    /**
91     * Simulation of (?int)$value
92     *
93     * @note Could be replaced with https://wiki.php.net/rfc/nullable-casting, if it ever becomes
94     * a part of PHP.
95     * @param mixed $value
96     * @return int|null Null if $value is null, otherwise (int)$value.
97     */
98    private function castToNullableInt( $value ): ?int {
99        if ( $value === null ) {
100            return null;
101        }
102
103        return (int)$value;
104    }
105
106    /**
107     * Get praiseworthy conditions for a mentor
108     *
109     * Defaults are provided by Community configuration as:
110     *
111     *  1) GEPersonalizedPraiseMaxEdits: the maximum number of edits a mentee must have to be
112     *     praiseworthy
113     *  2) GEPersonalizedPraiseMinEdits: the minimum number of edits a mentee must have to be
114     *     praiseworthy
115     *  3) GEPersonalizedPraiseDays: to be considered praiseworthy, a mentee needs to make a
116     *     certain number of edits (see above) in this amount of days to be praiseworthy.
117     *
118     * @param UserIdentity $user
119     * @return PraiseworthyConditions
120     */
121    public function getPraiseworthyConditions( UserIdentity $user ): PraiseworthyConditions {
122        $settings = $this->loadSettings( $user );
123
124        return new PraiseworthyConditions(
125            (int)( $settings[PraiseworthyConditions::SETTING_MAX_EDITS] ??
126                $this->wikiConfig->get( 'GEPersonalizedPraiseMaxEdits' ) ),
127            (int)( $settings[PraiseworthyConditions::SETTING_MIN_EDITS] ??
128                $this->wikiConfig->get( 'GEPersonalizedPraiseMinEdits' ) ),
129            $this->castToNullableInt( $settings[PraiseworthyConditions::SETTING_MAX_REVERTS] ??
130                $this->wikiConfig->get( 'GEPersonalizedPraiseMaxReverts' ) ),
131            (int)( $settings[PraiseworthyConditions::SETTING_DAYS] ??
132                $this->wikiConfig->get( 'GEPersonalizedPraiseDays' ) ),
133        );
134    }
135
136    /**
137     * Get default subject for the praising message
138     *
139     * @param UserIdentity $user
140     * @return string
141     */
142    public function getPraisingMessageDefaultSubject( UserIdentity $user ): string {
143        return $this->loadSettings( $user )[ self::SETTING_MESSAGE_SUBJECT ] ?? $this->messageLocalizer->msg(
144            'growthexperiments-mentor-dashboard-personalized-praise-praise-message-title'
145        )->inContentLanguage()->text();
146    }
147
148    /**
149     * Get user subpage title where the user-specific praising message is stored
150     *
151     * Unlike all other options in PersonalizedPraiseSettings, the praising message is stored on
152     * a user subpage, to make use of native MediaWiki preloading.
153     *
154     * Title returned by this method is not guaranteed to be known.
155     *
156     * @param UserIdentity $user
157     * @return Title
158     */
159    public function getPraisingMessageUserTitle( UserIdentity $user ): Title {
160        return $this->userFactory->newFromUserIdentity( $user )
161            ->getUserPage()
162            ->getSubpage( self::USER_MESSAGE_PRELOAD_SUBPAGE_NAME );
163    }
164
165    /**
166     * Get title that currently defines where the praising message is defined
167     *
168     * If it exists, a subpage of the $user is returned. Otherwise, a page in NS_MEDIAWIKI is
169     * returned instead.
170     *
171     * @param UserIdentity $user
172     * @return Title
173     */
174    public function getPraisingMessageTitle( UserIdentity $user ): Title {
175        $userSubpage = $this->getPraisingMessageUserTitle( $user );
176        return $userSubpage->exists() ? $userSubpage : $this->titleFactory->newFromTextThrow(
177            'growthexperiments-mentor-dashboard-personalized-praise-praise-message-message',
178            NS_MEDIAWIKI
179        );
180    }
181
182    /**
183     * @param UserIdentity $user
184     * @return string
185     */
186    public function getPraisingMessageContent( UserIdentity $user ): string {
187        $revision = $this->revisionLookup->getRevisionByTitle( $this->getPraisingMessageTitle( $user ) );
188        if ( !$revision ) {
189            return '';
190        }
191        $content = $revision->getContent( SlotRecord::MAIN );
192        if ( !$content instanceof WikitextContent ) {
193            return '';
194        }
195
196        return $content->getText();
197    }
198
199    /**
200     * How frequently should the user be notified about new praiseworthy mentees?
201     *
202     * @param UserIdentity $user
203     * @return int Minimum number of hours that needs to pass since the last notification
204     * (values specified by PersonalizedPraiseSettings::NOTIFY_* constants have special meaning)
205     */
206    public function getNotificationsFrequency( UserIdentity $user ): int {
207        return $this->loadSettings( $user )[self::SETTING_NOTIFICATION_FREQUENCY]
208            ?? (int)$this->wikiConfig->get( 'GEPersonalizedPraiseDefaultNotificationsFrequency' );
209    }
210}