Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
LinkSubmissionRecorder
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 4
56
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 record
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
6
 log
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 titlesToPageIds
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace GrowthExperiments\NewcomerTasks\AddLink;
4
5use IDBAccessObject;
6use ManualLogEntry;
7use MediaWiki\Cache\LinkBatchFactory;
8use MediaWiki\Title\TitleParser;
9use MediaWiki\User\UserIdentity;
10use StatusValue;
11
12/**
13 * Record information about a user accepting/rejecting parts of a link recommendation.
14 * This involves creating a log entry and updating an article-specific exclusion list.
15 */
16class LinkSubmissionRecorder {
17
18    /** @var TitleParser */
19    private $titleParser;
20
21    /** @var LinkBatchFactory */
22    private $linkBatchFactory;
23
24    /** @var LinkRecommendationStore */
25    private $linkRecommendationStore;
26
27    /**
28     * @param TitleParser $titleParser
29     * @param LinkBatchFactory $linkBatchFactory
30     * @param LinkRecommendationStore $linkRecommendationStore
31     */
32    public function __construct(
33        TitleParser $titleParser,
34        LinkBatchFactory $linkBatchFactory,
35        LinkRecommendationStore $linkRecommendationStore
36    ) {
37        $this->titleParser = $titleParser;
38        $this->linkBatchFactory = $linkBatchFactory;
39        $this->linkRecommendationStore = $linkRecommendationStore;
40    }
41
42    /**
43     * Record the results of a user reviewing a link recommendation.
44     * @param UserIdentity $user
45     * @param LinkRecommendation $linkRecommendation
46     * @param string[] $acceptedTargets
47     * @param string[] $rejectedTargets
48     * @param string[] $skippedTargets
49     * @param int|null $editRevId Revision ID of the edit adding the links (might be null since
50     *   it's not necessary that any links have been added).
51     * @return StatusValue
52     */
53    public function record(
54        UserIdentity $user,
55        LinkRecommendation $linkRecommendation,
56        array $acceptedTargets,
57        array $rejectedTargets,
58        array $skippedTargets,
59        ?int $editRevId
60    ): StatusValue {
61        if ( $this->linkRecommendationStore->hasSubmission( $linkRecommendation,
62            IDBAccessObject::READ_LOCKING )
63        ) {
64            // There's already a review for this revision. Possibly a race condition where two
65            // users reviewed the same task at the same time.
66            return StatusValue::newGood()->error( 'growthexperiments-addlink-duplicatesubmission',
67                $linkRecommendation->getRevisionId() );
68        }
69        $this->linkRecommendationStore->recordSubmission(
70            $user,
71            $linkRecommendation,
72            $this->titlesToPageIds( $acceptedTargets ),
73            $this->titlesToPageIds( $rejectedTargets ),
74            $this->titlesToPageIds( $skippedTargets ),
75            $editRevId
76        );
77        $logId = $this->log(
78            $user,
79            $linkRecommendation,
80            count( $acceptedTargets ),
81            count( $rejectedTargets ),
82            count( $skippedTargets ),
83            $editRevId
84        );
85        return StatusValue::newGood( [ 'logId' => $logId ] );
86    }
87
88    /**
89     * Make a Special:Log entry.
90     * @param UserIdentity $user
91     * @param LinkRecommendation $linkRecommendation
92     * @param int $acceptedLinkCount
93     * @param int $rejectedLinkCount
94     * @param int $skippedLinkCount
95     * @return int Log ID.
96     * @param int|null $editRevId Revision ID of the edit adding the links (might be null since
97     *   it's not necessary that any links have been added).
98     */
99    private function log(
100        UserIdentity $user,
101        LinkRecommendation $linkRecommendation,
102        int $acceptedLinkCount,
103        int $rejectedLinkCount,
104        int $skippedLinkCount,
105        ?int $editRevId
106    ): int {
107        $totalLinkCount = $acceptedLinkCount + $rejectedLinkCount + $skippedLinkCount;
108
109        $logEntry = new ManualLogEntry( 'growthexperiments', 'addlink' );
110        $logEntry->setTarget( $linkRecommendation->getTitle() );
111        $logEntry->setPerformer( $user );
112        $logEntry->setParameters( [
113            '4:number:count' => $totalLinkCount,
114            '5:number:count' => $acceptedLinkCount,
115            '6:number:count' => $rejectedLinkCount,
116            '7:number:count' => $skippedLinkCount,
117        ] );
118        if ( $editRevId ) {
119            // This has the side effect of the log entry getting tagged with all the change tags
120            // the revision is getting tagged with. Overall, still preferable - the log entry is
121            // not published to recent changes so its tags don't matter much.
122            $logEntry->setAssociatedRevId( $editRevId );
123        }
124        $logId = $logEntry->insert();
125        // Do not publish to recent changes, it would be pointless as this action cannot
126        // be inspected or patrolled.
127        $logEntry->publish( $logId, 'udp' );
128        return $logId;
129    }
130
131    /**
132     * Converts title strings to page IDs. Non-existent pages are omitted.
133     * @param string[] $titles
134     * @return int[]
135     */
136    private function titlesToPageIds( array $titles ): array {
137        $linkBatch = $this->linkBatchFactory->newLinkBatch();
138        foreach ( $titles as $title ) {
139            // ensuring that the title is valid is left to the caller
140            $linkBatch->addObj( $this->titleParser->parseTitle( $title ) );
141        }
142        $ids = $linkBatch->execute();
143        // LinkBatch::execute() returns a title => ID map. Discard titles, discard
144        // 0 ID used for non-existent pages (we assume those won't be recommended anyway),
145        // squash duplicates (just in case; they shouldn't exist).
146        return array_unique( array_filter( array_values( $ids ) ) );
147    }
148
149}