From 3382a1c833f590f856b5f3fa7c0711f08456204d Mon Sep 17 00:00:00 2001 From: krisfield Date: Fri, 15 Feb 2019 13:58:32 -0600 Subject: [PATCH] Show pending Approved Revs in Pending Reviews (#98) * Optionally allow create PendingReview with Title * Create PendingApproval class * Improve title of pending approvals * Add green star next to pending approved revs * show approved revID * diff link for approvals --- Hooks.php | 22 +++- extension.json | 1 + i18n/en.json | 10 +- includes/PendingApproval.php | 100 +++++++++++++++++ includes/PendingReview.php | 50 ++++----- modules/base/ext.watchanalytics.base.css | 4 + specials/SpecialPendingReviews.php | 133 ++++++++++++++++++++--- 7 files changed, 274 insertions(+), 46 deletions(-) create mode 100644 includes/PendingApproval.php diff --git a/Hooks.php b/Hooks.php index 66453d8..3172383 100644 --- a/Hooks.php +++ b/Hooks.php @@ -27,6 +27,13 @@ public static function onPersonalUrls( array &$personal_urls ) { $numPending = $watchStats['num_pending']; $maxPendingDays = $watchStats['max_pending_days']; + // Get user's pending approvals + // Check that Approved Revs is installed + $numPendingApprovals = 0; + if ( class_exists( 'ApprovedRevs' ) ) { + $numPendingApprovals = count( PendingApproval::getUserPendingApprovals( $user ) ); + } + // Determine CSS class of Watchlist/PendingReviews link $personal_urls['watchlist']['class'] = [ 'mw-watchanalytics-watchlist-badge' ]; if ( $numPending != 0 ) { @@ -37,10 +44,19 @@ public static function onPersonalUrls( array &$personal_urls ) { global $egPendingReviewsEmphasizeDays; if ( $maxPendingDays > $egPendingReviewsEmphasizeDays ) { $personal_urls['watchlist']['class'][] = 'mw-watchanalytics-watchlist-pending-old'; - $text = wfMessage( 'watchanalytics-personal-url-old' )->params( $numPending, $maxPendingDays )->text(); + if ( $numPendingApprovals != 0 ) { + $text = wfMessage( 'watchanalytics-personal-url-approvals-old' )->params( $numPending, $maxPendingDays, $numPendingApprovals )->text(); + } else { + $text = wfMessage( 'watchanalytics-personal-url-old' )->params( $numPending, $maxPendingDays )->text(); + } } else { - // when $sk (third arg) available, replace wfMessage with $sk->msg() - $text = wfMessage( 'watchanalytics-personal-url' )->params( $numPending )->text(); + if ( $numPendingApprovals != 0 ) { + $text = wfMessage( 'watchanalytics-personal-url-approvals' )->params( $numPending, $numPendingApprovals )->text(); + } else { + // when $sk (third arg) available, replace wfMessage with $sk->msg() + $text = wfMessage( 'watchanalytics-personal-url' )->params( $numPending )->text(); + } + } $personal_urls['watchlist']['text'] = $text; diff --git a/extension.json b/extension.json index 5048cc2..7c5caae 100644 --- a/extension.json +++ b/extension.json @@ -41,6 +41,7 @@ "WatchAnalyticsUser": "WatchAnalyticsUser.php", "WatchAnalyticsUpdaterHooks": "schema/WatchAnalyticsUpdaterHooks.php", "PendingReview": "includes/PendingReview.php", + "PendingApproval": "includes/PendingApproval.php", "WatchSuggest": "includes/WatchSuggest.php", "ReviewHandler": "includes/ReviewHandler.php", "PageScore": "includes/PageScore.php", diff --git a/i18n/en.json b/i18n/en.json index 42d931f..2a0d49b 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -6,7 +6,9 @@ }, "watchanalytics-desc": "Encouraging good distribution of watchers", "watchanalytics-personal-url": "Pending reviews ($1)", + "watchanalytics-personal-url-approvals": "Pending reviews ($1)/approvals ($2)", "watchanalytics-personal-url-old": "{{PLURAL:$1|1 review|$1 reviews}} (oldest: {{PLURAL:$2|1 day|$2 days}})", + "watchanalytics-personal-url-approvals-old": "{{PLURAL:$1|1 review|$1 reviews}} (oldest: {{PLURAL:$2|1 day|$2 days}})/approvals ($3)", "watchanalytics-view": "View:", "pendingreviews": "Pending reviews", "watchanalytics": "Watch analytics", @@ -76,6 +78,7 @@ "watchanalytics-pause-visualization": "Pause visualization", "watchanalytics-unpause-visualization": "Unpause visualization", "watchanalytics-pendingreviews-diff-revisions": "Display {{PLURAL:$1|1 change|$1 changes}} since last visit", + "watchanalytics-view-and-approve": "View/Approve changes", "watchanalytics-pendingreviews-users-first-view": "New page - view latest", "watchanalytics-pendingreviews-history-link": "view page history", "watchanalytics-pendingreviews-prev-revisions": "< Previous", @@ -85,7 +88,9 @@ "pendingreviews-timediff-minutes": "Changed {{PLURAL:$1|1 minute|$1 minutes}} ago", "pendingreviews-timediff-just-now": "Changed just now", "pendingreviews-no-revisions": "No page content changes", - "pendingreviews-num-reviews": "You have $1 pending {{PLURAL:$1|review|reviews}}.", + "pendingreviews-num-reviews": "You have $1 pending {{PLURAL:$1|review|reviews}}", + "pendingreviews-num-reviews-complete": "Congrats! You completed your reviews." , + "pendingreviews-num-approvals": "/$1 pending {{PLURAL:$1|approval|approvals}}.", "pendingreviews-reviewer-criticality-danger": "Pages reviewed by 0 - {{PLURAL:$1|1 person|$1 people}}", "pendingreviews-reviewer-criticality-danger-zero": "Pages reviewed by 0 people", "pendingreviews-reviewer-criticality-generic": "Pages reviewed by $1 or more people", @@ -109,6 +114,8 @@ "watchanalytics-view-user-pendingreviews": "pending reviews", "pendingreviews-watch-suggestion-thanks": "Thanks for watching!", + "pendingreviews-pending-approvedrev": "Has revisions which require approval", + "pendingreviews-pending-approvedrev-title": "Revision approval required: $1", "pendingreviews-edited-by": "[[$1]] made an edit '''without a summary'''", "pendingreviews-with-comment": "[[$1]] made an edit with summary: ", "pendingreviews-page-deleted": "Deleted page: $1", @@ -135,6 +142,7 @@ "log-description-pendingreviews": "Tracks actions taken using tools from the Watch Analytics extension related to Pending Reviews.", "logentry-pendingreviews-clearreivews": "$1 {{GENDER:$2|cleared $4 pending reviews}} with criteria -category$5 -title like$6 using $3", "pendingreviews-watch-suggestion-title": "This wiki needs your help watching pages", + "pendingreviews-approve-revs-title": "{{PLURAL:$1|Page|Pages}} needing your approval ($1)", "pendingreviews-watch-suggestion-description": "Few (if any) people are watching the pages below. They are related to other pages in your watchlist, and it'd be real swell if you could help by watching some of them.", "pendingreviews-watch-suggestion-watchlink": "Watch" diff --git a/includes/PendingApproval.php b/includes/PendingApproval.php new file mode 100644 index 0000000..7b0cf31 --- /dev/null +++ b/includes/PendingApproval.php @@ -0,0 +1,100 @@ +title = $title; + + $this->notificationTimestamp = $row['notificationtimestamp']; + $this->numReviewers = intval( $row['num_reviewed'] ); + + // Keep these just to be consistent with PendingReview class + $this->deletedTitle = false; + $this->deletedNS = false; + $this->deletionLog = false; + + // FIXME + // no log for now, maybe link to approval log + // no list of revisions for now + $this->log = []; + $this->newRevisions = []; + } + + /** + * Get an array of pages user can approve that require approvals + * @param User $user + * @return Array + */ + public static function getUserPendingApprovals( User $user ) { + $dbr = wfGetDB( DB_REPLICA ); + + $queryInfo = ApprovedRevs::getQueryInfoPageApprovals( 'notlatest' ); + $latestNotApproved = $dbr->select( + $queryInfo['tables'], + $queryInfo['fields'], + $queryInfo['conds'], + __METHOD__, + $queryInfo['options'], + $queryInfo['join_conds'] + ); + $pagesUserCanApprove = []; + + while ( $page = $latestNotApproved->fetchRow() ) { + + // $page with keys id, rev_id, latest_id + $title = Title::newFromID( $page['id'] ); + + if ( ApprovedRevs::userCanApprove( $user, $title ) ) { + + // FIXME: May want to get these in there so PendingReviews can + // show the list of revs in the approval. + // 'approved_rev_id' => $page['rev_id'] + // 'latest_rev_id' => $page['latest_id'] + $pagesUserCanApprove[] = new self( + [ + 'notificationtimestamp' => null, + 'num_reviewed' => 0, // if page has pending approval, zero people have approved + ], + $title + ); + + } + + } + + return $pagesUserCanApprove; + } + +} diff --git a/includes/PendingReview.php b/includes/PendingReview.php index f73802f..d00a28b 100644 --- a/includes/PendingReview.php +++ b/includes/PendingReview.php @@ -47,14 +47,26 @@ class PendingReview { */ public $log; - public function __construct( $row ) { - $pageID = $row['page_id']; + public function __construct( $row, Title $title = null ) { $notificationTimestamp = $row['notificationtimestamp']; - if ( $pageID ) { - $title = Title::newFromID( $pageID ); + $this->notificationTimestamp = $notificationTimestamp; + $this->numReviewers = intval( $row['num_reviewed'] ); + + if ( $title ) { + $pageID = $title->getArticleID(); + $namespace = $title->getNamespace(); + $titleDBkey = $title->getDBkey(); } else { - $title = false; + $pageID = $row['page_id']; + $namespace = $row['namespace']; + $titleDBkey = $row['title']; + + if ( $pageID ) { + $title = Title::newFromID( $pageID ); + } else { + $title = false; + } } if ( $pageID && $title->exists() ) { @@ -64,14 +76,6 @@ public function __construct( $row ) { $revResults = $dbr->select( [ 'r' => 'revision' ], Revision::getQueryInfo()['fields'], - // array( - // 'r.rev_id AS rev_id', - // 'r.rev_comment AS rev_comment', - // 'r.rev_user AS rev_user_id', - // 'r.rev_user_text AS rev_user_name', - // 'r.rev_timestamp AS rev_timestamp', - // 'r.rev_len AS rev_len', - // ), "r.rev_page=$pageID AND r.rev_timestamp>=$notificationTimestamp", __METHOD__, [ 'ORDER BY' => 'rev_timestamp ASC' ], @@ -85,14 +89,6 @@ public function __construct( $row ) { $logResults = $dbr->select( [ 'l' => 'logging' ], [ '*' ], - // array( - // 'l.log_id AS log_id', - // 'l.log_type AS log_type', - // 'l.log_action AS log_action', - // 'l.log_timestamp AS log_timestamp', - // 'l.log_user AS log_user_id', - // 'l.log_user_text AS log_user_name', - // ), "l.log_page=$pageID AND l.log_timestamp>=$notificationTimestamp AND l.log_type NOT IN ('interwiki','newusers','patrol','rights','upload')", __METHOD__, @@ -109,21 +105,19 @@ public function __construct( $row ) { $deletionLog = false; } else { - $deletedNS = $row['namespace']; - $deletedTitle = $row['title']; + $deletedNS = $namespace; + $deletedTitle = $titleDBkey; $deletionLog = $this->getDeletionLog( $deletedTitle, $deletedNS, $notificationTimestamp ); $logPending = false; $revsPending = false; } - $this->notificationTimestamp = $notificationTimestamp; $this->title = $title; $this->newRevisions = $revsPending; $this->deletedTitle = $deletedTitle; $this->deletedNS = $deletedNS; $this->deletionLog = $deletionLog; $this->log = $logPending; - $this->numReviewers = intval( $row['num_reviewed'] ); } public static function getPendingReviewsList( User $user, $limit, $offset ) { @@ -188,6 +182,12 @@ public static function getPendingReviewsList( User $user, $limit, $offset ) { } + // If ApprovedRevs is installed, append any pages in need of approvals + // to the front of the Pending Reviews list + if ( class_exists( 'ApprovedRevs' ) ) { + $pending = array_merge( PendingApproval::getUserPendingApprovals( $user ), $pending ); + } + return $pending; } diff --git a/modules/base/ext.watchanalytics.base.css b/modules/base/ext.watchanalytics.base.css index 6f242d1..5899efa 100644 --- a/modules/base/ext.watchanalytics.base.css +++ b/modules/base/ext.watchanalytics.base.css @@ -75,3 +75,7 @@ tr.ext-watchanalytics-criticality-okay td:first-child { tr.ext-watchanalytics-criticality-excellent td:first-child { border-left: solid #00af89 5px; } + +tr.ext-watchanalytics-approvable-page td:first-child { + border-left: solid #00B050 5px; +} diff --git a/specials/SpecialPendingReviews.php b/specials/SpecialPendingReviews.php index e8cdc0f..5c0219c 100644 --- a/specials/SpecialPendingReviews.php +++ b/specials/SpecialPendingReviews.php @@ -125,27 +125,60 @@ public function execute( $parser = null ) { $this->pendingReviewList = PendingReview::getPendingReviewsList( $this->mUser, $this->reviewLimit, $this->reviewOffset ); - $html = $this->getPageHeader( $wgUser ); + // Check that Approved Revs is installed + $useApprovedRevs = class_exists( 'ApprovedRevs' ); + + $html = $this->getPageHeader( $wgUser, $useApprovedRevs ); $html .= ''; $rowCount = 0; // loop through pending reviews foreach ( $this->pendingReviewList as $item ) { - // if the title exists, then the page exists (and hence it has not - // been deleted) - if ( $item->title ) { - $html .= $this->getStandardChangeRow( $item, $rowCount ); - // page has been deleted (or moved w/o a redirect) + if ( $useApprovedRevs && is_a( $item, 'PendingApproval' ) ) { + // don't add approvals here + continue; + } elseif ( $item->title ) { + // if the title exists, then the page exists (and hence it has not + // been deleted) + $html .= $this->getStandardChangeRow( $item, $rowCount ); + $rowCount++; } else { + // page has been deleted (or moved w/o a redirect) $html .= $this->getDeletedPageRow( $item, $rowCount ); + $rowCount++; } - $rowCount++; } $html .= '
'; + // how many pending approval rows there are + $approvedRowCount = 0; + + if ( $useApprovedRevs ) { + $numApprovedRevs = count( PendingApproval::getUserPendingApprovals( $this->mUser ) ); + + if ( $numApprovedRevs != 0 ) { + $html .= '

' . wfMessage( 'pendingreviews-approve-revs-title', $numApprovedRevs )->parse() . '

'; + $html .= ''; + + // loop through pending reviews + foreach ( $this->pendingReviewList as $item ) { + + // if ApprovedRevs installed... + if ( $useApprovedRevs && is_a( $item, 'PendingApproval' ) ) { + $html .= $this->getApprovedRevsChangeRow( $item, $approvedRowCount ); + } + + $approvedRowCount++; + } + $html .= '
'; + + } + + } + global $egPendingReviewsShowWatchSuggestionsIfReviewsUnder; // FIXME: crazy long name... if ( $rowCount < $egPendingReviewsShowWatchSuggestionsIfReviewsUnder ) { $watchSuggest = new WatchSuggest( $this->mUser ); @@ -277,7 +310,7 @@ public function getStandardChangeRow( PendingReview $item, $rowCount ) { $displayTitle = '' . $item->title->getFullText() . ''; - return $this->getRowHTML( $item, $rowCount, $displayTitle, $reviewButton, $historyButton, $changes ); + return $this->getReviewRowHTML( $item, $rowCount, $displayTitle, $reviewButton, $historyButton, $changes ); } /** @@ -319,7 +352,31 @@ public function getDeletedPageRow( PendingReview $item, $rowCount ) { . wfMessage( $displayMessage, $title->getFullText() )->parse() . ''; - return $this->getRowHTML( $item, $rowCount, $displayTitle, $acceptDeletionButton, $talkToDeleterButton, $changes ); + return $this->getReviewRowHTML( $item, $rowCount, $displayTitle, $acceptDeletionButton, $talkToDeleterButton, $changes ); + } + + /** + * Generates row for a pending ApprovedRevs revision. + * + * @param PendingReview $item + * @param int $approvedRowCount used to determine if the row is odd or even + * @return string HTML for row + */ + public function getApprovedRevsChangeRow( PendingReview $item, $approvedRowCount ) { + $changes = ''; + + $buttonOne = ''; + + $historyButton = $this->getApproveButton( $item ); + + $approvedRevID = ApprovedRevs::getApprovedRevID( $item->title ); + + $displayTitle = '' . + ' ' . + $item->title->getFullText() . + ''; + + return $this->getApproveRowHTML( $item, $approvedRowCount, $displayTitle, $buttonOne, $historyButton, $changes ); } /** @@ -333,7 +390,7 @@ public function getDeletedPageRow( PendingReview $item, $rowCount ) { * @param string $changes * @return string HTML for pending review of a given page */ - public function getRowHTML( PendingReview $item, $rowCount, $displayTitle, $buttonOne, $buttonTwo, $changes ) { + public function getReviewRowHTML( PendingReview $item, $rowCount, $displayTitle, $buttonOne, $buttonTwo, $changes ) { // FIXME: wow this is ugly $rowClass = ( $rowCount % 2 === 0 ) ? 'pendingreviews-even-row' : 'pendingreviews-odd-row'; @@ -362,6 +419,24 @@ public function getRowHTML( PendingReview $item, $rowCount, $displayTitle, $butt return $html; } + public function getApproveRowHTML( PendingReview $item, $approvedRowCount, $displayTitle, $buttonOne, $buttonTwo, $changes ) { + // FIXME: wow this is ugly + $rowClass = ( $approvedRowCount % 2 === 0 ) ? 'pendingreviews-even-row' : 'pendingreviews-odd-row'; + + $classAndAttr = "class='pendingreviews-row $rowClass " . + "ext-watchanalytics-approvable-page pendingreviews-row-$approvedRowCount' " . + "pendingreviews-row-count='$approvedRowCount'"; + + $html = "" . + "$displayTitle" . + "" . + "$buttonOne $buttonTwo"; + + $html .= "$changes"; + + return $html; + } + /** * Creates a button bringing user to the diff page. * @@ -407,6 +482,28 @@ public function getReviewButton( $item ) { return $diffLink; } + /** + * Creates a button bringing user to view diff since last approved version + * + * @param PendingReview $item + * @return string HTML for button + */ + public function getApproveButton( $item ) { + $diffURL = $item->title->getLocalURL( [ + 'diff' => '', + 'oldid' => ApprovedRevs::getApprovedRevID( $item->title ) + ] ); + + $diffLink = Xml::element( 'a', + [ 'href' => $diffURL, 'class' => 'pendingreviews-green-button', 'target' => "_blank" ], + wfMessage( + 'watchanalytics-view-and-approve' + )->text() + ); + + return $diffLink; + } + /** * Creates a button bringing user to the history page. * @@ -545,9 +642,10 @@ public function getDeleterTalkButton( array $deletionLog ) { * Creates simple header stating how many pending reviews the user has. * * @param User $user + * @param bool $useApprovedRevs * @return string HTML for header */ - public function getPageHeader( User $user ) { + public function getPageHeader( User $user, $useApprovedRevs ) { $userWatch = new UserWatchesQuery(); $watchStats = $userWatch->getUserWatchStats( $user ); $numPendingReviews = $watchStats['num_pending']; @@ -558,12 +656,6 @@ public function getPageHeader( User $user ) { $html .= $this->getPendingReviewsLegend(); } - // message like "You have X pending reviews" - $html .= '

' . wfMessage( 'pendingreviews-num-reviews', $numPendingReviews, $this->reviewLimit )->text(); - - // close out header - $html .= '

'; - $nextReviewSet = $this->reviewOffset + $this->reviewLimit; $prevReviewSet = max( [ 0, $this->reviewOffset - $this->reviewLimit ] ); $currentURL = $this->getPageTitle()->getLocalUrl(); @@ -603,6 +695,13 @@ public function getPageHeader( User $user ) { wfMessage( 'watchanalytics-pendingreviews-next-revisions' )->text() ); + if ( $numPendingReviews != 0 ) { + // message like "You have X pending reviews" + $html .= '

' . wfMessage( 'pendingreviews-num-reviews', $numPendingReviews, $this->reviewLimit )->text() . '

'; + } else { + $html .= '
' . wfMessage( 'pendingreviews-num-reviews-complete' )->text() . ''; + } + return $html; }