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