diff --git a/Hooks.php b/Hooks.php index 66453d8..69d5710 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; @@ -55,7 +71,7 @@ public static function onPersonalUrls( array &$personal_urls ) { * * 1) Determine if user should see shaky pending reviews link * 2) Insert page scores on applicable pages - * 3) REMOVED FOR MW 1.27: If a page review has occured on this page view, display an unreview + * 3) REMOVED FOR MW 1.27: If a page review has occurred on this page view, display an unreview * option and record that the review happened. * * Also supports parameter: Skin $skin. @@ -243,8 +259,10 @@ public static function handleMagicWords( &$parser, &$text ) { * @return bool */ public static function onPageViewUpdates( WikiPage $wikiPage, User $user ) { + global $wgRequest; $title = $wikiPage->getTitle(); - $reviewHandler = ReviewHandler::setup( $user, $title ); + $isDiff = $wgRequest->getText( 'oldid' ); + $reviewHandler = ReviewHandler::setup( $user, $title, $isDiff ); if ( $reviewHandler::pageIsBeingReviewed() ) { diff --git a/extension.json b/extension.json index 5048cc2..f11b12f 100644 --- a/extension.json +++ b/extension.json @@ -1,6 +1,6 @@ { "name": "WatchAnalytics", - "version": "2.0.1", + "version": "3.1.0", "author": [ "[https://www.mediawiki.org/wiki/User:Jamesmontalvo3 James Montalvo]" ], @@ -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", @@ -205,6 +206,9 @@ "config": { "_prefix": "eg", "WatchAnalyticsPageCounter": false, + "WatchAnalyticsShowUnreviewDiff": true, + "PendingReviewMaxDiffChar": 3500, + "PendingReviewMaxDiffRows": 15, "PendingReviewsEmphasizeDays": 7, "PendingReviewsRedPagesThreshold": 2, "PendingReviewsOrangePagesThreshold": 4, diff --git a/i18n/en.json b/i18n/en.json index 42d931f..ca04b2a 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", @@ -75,7 +77,8 @@ "watchanalytics-watch-forcegraph-description": "Orange dots represent users. Blue dots represent pages. Lines between the dots represent the user being a watcher of the page. Lines are gray if the latest page revision has been reviewed. Lines are red if reviews are pending. Move your mouse over a dot to see the user or page name.", "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-pendingreviews-diff-revisions": "View {{PLURAL:$1|change|$1 changes}} on page", + "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,10 @@ "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-other-user-reviews": "$1 has $2 pending {{PLURAL:$2|review|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", @@ -103,12 +109,15 @@ "watchanalytics-pagestats-chart-header": "Number of reviewers over time", "watchanalytics-unreview-button": "Defer review", + "watchanalytics-accept-change-close-banner": "Close banner", "watchanalytics-unreview-banner-text": "'''Page reviewed!''' By navigating to this page you have marked it reviewed. If you did not want to review the page you may un-review it to save it for another time.", "watchanalytics-unreview-complete": "'''This page has been un-reviewed''' and is again in your [[Special:PendingReviews|Pending Reviews]].", "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", @@ -117,6 +126,7 @@ "pendingreviews-accept-deletion": "Accept deletion", "pendingreviews-accept-move-without-redirect": "Accept move without redirect", "pendingreviews-accept-redirect": "Accept redirect", + "pendingreviews-accept-change": "Mark reviewed", "pendingreviews-clear-page-notification": "Page \"$1\" has been marked reviewed. Return to $2", "pendingreviews-log-approved": "[[$1]] approved a revision", "pendingreviews-log-unapproved": "[[$1]] removed all revision approvals", @@ -135,6 +145,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..56d3cbf 100644 --- a/includes/PendingReview.php +++ b/includes/PendingReview.php @@ -1,5 +1,7 @@ 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() ) { $dbr = wfGetDB( DB_REPLICA ); + $revisionStore = MediaWikiServices::getInstance()->getRevisionStore(); + + $revQueryInfo = $revisionStore->getQueryInfo(); + $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", + $revQueryInfo['tables'], + $revQueryInfo['fields'], + "rev_page=$pageID AND rev_timestamp>=$notificationTimestamp", __METHOD__, [ 'ORDER BY' => 'rev_timestamp ASC' ], - null + $revQueryInfo['joins'] ); $revsPending = []; while ( $rev = $revResults->fetchObject() ) { @@ -85,14 +95,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 +111,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,37 +188,104 @@ 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; } - public function getDeletionLog( $title, $ns, $notificationTimestamp ) { + public static function getPendingReview( User $user, Title $title ) { + $tables = [ + 'w' => 'watchlist', + 'p' => 'page', + 'log' => 'logging', + ]; + + $fields = [ + 'p.page_id AS page_id', + 'log.log_action AS log_action', + 'w.wl_namespace AS namespace', + 'w.wl_title AS title', + 'w.wl_notificationtimestamp AS notificationtimestamp', + '(SELECT COUNT(*) FROM watchlist AS subwatch + WHERE + subwatch.wl_namespace = w.wl_namespace + AND subwatch.wl_title = w.wl_title + AND subwatch.wl_notificationtimestamp IS NULL + ) AS num_reviewed', + ]; + + $conds = [ 'w.wl_user' => $user->getId() , 'p.page_id' => $title->getArticleID() , 'w.wl_notificationtimestamp IS NOT NULL' ]; + + $options = []; + + $join_conds = [ + 'p' => [ + 'LEFT JOIN', 'p.page_namespace=w.wl_namespace AND p.page_title=w.wl_title' + ], + 'log' => [ + 'LEFT JOIN', + 'log.log_namespace = w.wl_namespace ' + . ' AND log.log_title = w.wl_title' + . ' AND p.page_namespace IS NULL' + . ' AND p.page_title IS NULL' + . ' AND log.log_action IN ("delete","move")' + ], + ]; + $dbr = wfGetDB( DB_REPLICA ); + $watchResult = $dbr->select( + $tables, + $fields, + $conds, + __METHOD__, + $options, + $join_conds + ); + + $pending = []; + + while ( $row = $dbr->fetchRow( $watchResult ) ) { + + $pending[] = new self( $row ); + + } + return $pending; + } + + public function getDeletionLog( $title, $ns, $notificationTimestamp ) { + $dbr = wfGetDB( DB_REPLICA ); $title = $dbr->addQuotes( $title ); // pages are deleted when (a) they are explicitly deleted or (b) they // are moved without leaving a redirect behind. $logResults = $dbr->select( - [ 'l' => 'logging' ], + [ 'l' => 'logging', 'c' => 'comment' ], [ - 'log_id', - 'log_type', - 'log_action', - 'log_timestamp', - 'log_user', - 'log_user_text', - 'log_namespace', - 'log_title', - 'log_page', - 'log_comment', - 'log_params', - 'log_deleted', + 'l.log_id', + 'l.log_type', + 'l.log_action', + 'l.log_timestamp', + 'l.log_user', + 'l.log_user_text', + 'l.log_namespace', + 'l.log_title', + 'l.log_page', + 'l.log_comment_id', + 'l.log_params', + 'l.log_deleted', + 'c.comment_id', + 'c.comment_text AS log_comment' ], "l.log_title=$title AND l.log_namespace=$ns AND l.log_timestamp>=$notificationTimestamp AND l.log_type IN ('delete','move')", __METHOD__, - [ 'ORDER BY' => 'log_timestamp ASC' ], - null + [ 'ORDER BY' => 'l.log_timestamp ASC' ], + [ 'c' => [ 'INNER JOIN', [ 'l.log_comment_id=c.comment_id' ] ] ] ); $logDeletes = []; while ( $log = $logResults->fetchObject() ) { diff --git a/includes/ReviewHandler.php b/includes/ReviewHandler.php index 0302ecb..4ad9483 100644 --- a/includes/ReviewHandler.php +++ b/includes/ReviewHandler.php @@ -30,17 +30,18 @@ class ReviewHandler { */ public $final = null; - public function __construct( User $user, Title $title ) { + public function __construct( User $user, Title $title, $isDiff ) { $this->user = $user; $this->title = $title; + $this->isDiff = $isDiff; } - public static function setup( User $user, Title $title ) { + public static function setup( User $user, Title $title, $isDiff ) { if ( ! $title->isWatchable() ) { self::$isReviewable = false; return false; } - self::$pageLoadHandler = new self ( $user, $title ); + self::$pageLoadHandler = new self ( $user, $title, $isDiff ); self::$pageLoadHandler->initial = self::$pageLoadHandler->getReviewStatus(); return self::$pageLoadHandler; } @@ -116,20 +117,75 @@ public static function pageIsBeingReviewed() { public function getTemplate() { // $msg = wfMessage( 'watch-analytics-page-score-tooltip' )->text(); - $unReviewLink = SpecialPage::getTitleFor( 'PageStatistics' )->getInternalURL( [ - 'page' => $this->title->getPrefixedText(), - 'unreview' => $this->initial - ] ); + $reviewLink = Xml::element( + 'a', + [ + 'href' => null, + 'id' => 'watch-analytics-unreview', + 'class' => 'pendingreviews-green-button pendingreviews-accept-change', + ], + wfMessage( 'watchanalytics-accept-change-close-banner' )->text() + ); + + $unReviewLink = Xml::element( + 'a', + [ + 'href' => null, + 'id' => 'watch-analytics-unreview', + 'class' => 'watch-analytics-unreview', + 'timestamp' => $this->initial, + 'pending-title' => $this->title->getPrefixedText(), + 'title' => wfMessage( 'watchanalytics-unreview-button' )->text(), + ], + wfMessage( 'watchanalytics-unreview-button' )->text() + ); - $linkText = wfMessage( 'watchanalytics-unreview-button' )->text(); $bannerText = wfMessage( 'watchanalytics-unreview-banner-text' )->parse(); - // when MW 1.25 is released (very soon) replace this with a mustache template + $this->pendingReview = PendingReview::getPendingReview( $this->user, $this->title ); + + foreach ( $this->pendingReview as $item ) { + if ( count( $item->newRevisions ) > 0 ) { + + // returns essentially the negative-oneth revision...the one before + // the wl_notificationtimestamp revision...or null/false if none exists? + $mostRecentReviewed = Revision::newFromRow( $item->newRevisions[0] )->getPrevious(); + } else { + $mostRecentReviewed = false; // no previous revision, the user has not reviewed the first! + } + + if ( $mostRecentReviewed ) { + + $lastSeenId = $mostRecentReviewed->getId(); + + } else { + + $latest = Revision::newFromTitle( $item->title ); + $lastSeenId = $latest->getId(); + + } + + } + + $diff = new DifferenceEngine( null, $lastSeenId, 0 ); + $template = "
- $linkText -

$bannerText

-
"; + $unReviewLink $reviewLink +

$bannerText

"; + + global $egWatchAnalyticsShowUnreviewDiff; + if ( $egWatchAnalyticsShowUnreviewDiff ) { + // Don't show diff on in header while viewing diff page + if ( !( $this->isDiff ) ) { + $template .= "
"; + $template .= $diff->showDiffStyle(); + $template .= $diff->getDiff( 'Last seen', 'Current' ); + $template .= "
"; + } + } + + $template .= ""; return ""; } diff --git a/includes/WatchStateRecorder.php b/includes/WatchStateRecorder.php index 2bac059..fd50657 100644 --- a/includes/WatchStateRecorder.php +++ b/includes/WatchStateRecorder.php @@ -268,7 +268,7 @@ public function getWikiQueryInfo( $namespace = false, $prefix = '' ) { * @param WikiPage $wikipage * @return bool */ - public static function recordPageChange( WikiPage $wikipage ) { + public static function recordPageChange( $wikipage ) { $timestamp = date( "YmdHis", time() ); $title = $wikipage->getTitle(); diff --git a/modules/base/ext.watchanalytics.base.css b/modules/base/ext.watchanalytics.base.css index 6f242d1..35e8c48 100644 --- a/modules/base/ext.watchanalytics.base.css +++ b/modules/base/ext.watchanalytics.base.css @@ -20,10 +20,20 @@ color: #fff; } -tr.ext-watchanalytics-criticality-danger td:first-child { +tr.ext-watchanalytics-criticality-danger, td.ext-watchanalytics-criticality-danger{ border-left: solid #d33 5px; } +.pending-review-diff{ + padding: 8px 20px; + background-color: #fff; + border-radius: 4px; + margin: 0 0 20px 0; + border-style: solid; + border-width: 1px; + border-color: #eaecf0; +} + .ext-watchanalytics-criticality-plaid > div { background: repeating-linear-gradient( @@ -62,7 +72,7 @@ tr.ext-watchanalytics-criticality-danger td:first-child { color: #fff; } -tr.ext-watchanalytics-criticality-okay td:first-child { +tr.ext-watchanalytics-criticality-okay, td.ext-watchanalytics-criticality-okay { border-left: solid #fc3 5px; } @@ -72,6 +82,10 @@ tr.ext-watchanalytics-criticality-okay td:first-child { color: #fff; } -tr.ext-watchanalytics-criticality-excellent td:first-child { +tr.ext-watchanalytics-criticality-excellent, td.ext-watchanalytics-criticality-excellent { border-left: solid #00af89 5px; } + +tr.ext-watchanalytics-approvable-page td:first-child { + border-left: solid #00B050 5px; +} diff --git a/modules/pendingreviews/ext.watchanalytics.pendingreviews.css b/modules/pendingreviews/ext.watchanalytics.pendingreviews.css index 71b6941..6ea06ee 100644 --- a/modules/pendingreviews/ext.watchanalytics.pendingreviews.css +++ b/modules/pendingreviews/ext.watchanalytics.pendingreviews.css @@ -97,15 +97,23 @@ td.pendingreviews-review-links a:hover { } .pendingreviews-green-button { - background-color: #00af89; + background-color: #14866d; border-color: #00af89; } .pendingreviews-green-button:hover { - background-color: #14866d; + background-color: #116f5a; border-color: #00af89; } +.pendingreviews-green-button.pendingreviews-accept-change { + background-color: #00af89; +} + +.pendingreviews-green-button.pendingreviews-accept-change:hover { + background-color: #14866d; +} + .pendingreviews-orange-button { background-color: #fc3; border-color: #fc3; diff --git a/modules/pendingreviews/ext.watchanalytics.pendingreviews.js b/modules/pendingreviews/ext.watchanalytics.pendingreviews.js index 85243a2..1ba16a7 100644 --- a/modules/pendingreviews/ext.watchanalytics.pendingreviews.js +++ b/modules/pendingreviews/ext.watchanalytics.pendingreviews.js @@ -14,13 +14,13 @@ } ); }); - + $('.pendingreviews-accept-deletion').click( function( event ) { event.preventDefault(); var button = this; var title = $( button ).attr( 'pending-title' ), namespace = $( button ).attr( 'pending-namespace' ); - + new mw.Api().postWithToken( 'edit', { action: 'setnotificationtimestamp', titles: new mw.Title( title, namespace ).getPrefixedText() @@ -29,13 +29,36 @@ var rowLines = $( button ).closest( '.pendingreviews-row' ).add( $( button ).closest( '.pendingreviews-row' ).next() ); - - rowLines.fadeOut( 500, function() { + + rowLines.fadeOut( 300, function() { rowLines.remove(); }); - + } ); - + + }); + + $('.pendingreviews-accept-change').click( function( event ) { + event.preventDefault(); + var button = this; + var title = $( button ).attr( 'pending-title' ), + namespace = $( button ).attr( 'pending-namespace' ); + + new mw.Api().postWithToken( 'edit', { + action: 'setnotificationtimestamp', + titles: new mw.Title( title, namespace ).getPrefixedText() + } ).done( function ( data ) { + + var rowLines = $( button ).closest( '.pendingreviews-row' ).add( + $( button ).closest( '.pendingreviews-row' ).next() + ); + + rowLines.fadeOut( 300, function() { + rowLines.remove(); + }); + + } ); + }); $('.pendingreviews-watch-suggest-link').click( function ( event ) { @@ -52,9 +75,9 @@ } ).done( function ( data ) { $( button ).closest( 'li' ).html( thanks ); - + } ); }); -} )( jQuery, mediaWiki ); \ No newline at end of file +} )( jQuery, mediaWiki ); diff --git a/modules/reviewhandler/reviewhandler.css b/modules/reviewhandler/reviewhandler.css index 56685c7..8fcfeaf 100644 --- a/modules/reviewhandler/reviewhandler.css +++ b/modules/reviewhandler/reviewhandler.css @@ -1,12 +1,12 @@ #watch-analytics-review-handler { - padding: 8px 20px; + padding: 8px 20px; background-color: #fcf8e3; border-radius: 4px; margin: 0 0 20px 0; border-style: solid; border-width: 1px; border-color: #faebcc; - color: #8a6d3b; + color: #000; } #watch-analytics-unreview { @@ -35,3 +35,24 @@ background-color: #ec971f; border-color: #d58512; } + +#watch-analytics-unreview.pendingreviews-green-button.pendingreviews-accept-change { + background-color: #00af89; + border-color: #00af89; +} + +#watch-analytics-unreview.pendingreviews-green-button.pendingreviews-accept-change:hover { + background-color: #14866d; + border-color: #00af89; +} + +#diff-box { + padding: 8px 20px; + background-color: #fff; + border-radius: 4px; + margin: 0 0 20px 0; + border-style: solid; + border-width: 1px; + border-color: #ec971f; + color: #000; +} diff --git a/modules/reviewhandler/reviewhandler.js b/modules/reviewhandler/reviewhandler.js index 52523fe..5d597c7 100644 --- a/modules/reviewhandler/reviewhandler.js +++ b/modules/reviewhandler/reviewhandler.js @@ -8,6 +8,41 @@ $("#ext-watchanalytics-review-handler-template")[0].innerHTML ); + $('.watch-analytics-unreview').click( function( event ) { + event.preventDefault(); + var button = this; + var title = $( button ).attr( 'pending-title' ); + var notificaitonTimestamp = $( button ).attr( 'timestamp' ); + + new mw.Api().postWithToken( 'edit', { + action: 'setnotificationtimestamp', + timestamp: notificaitonTimestamp, + titles: title + } ).done( function ( data ) { + + var rowLines = $('#watch-analytics-review-handler' ); + + rowLines.html("Review deferred!"); + + rowLines.fadeOut( 700, function() { + rowLines.remove(); + }); + + } ); + + }); + + $('#watch-analytics-unreview.pendingreviews-green-button.pendingreviews-accept-change').click( function( event ) { + event.preventDefault(); + var button = this; + var rowLines = $('#watch-analytics-review-handler' ); + + rowLines.fadeOut( 700, function() { + rowLines.remove(); + }); + + }); + }); -} )( jQuery, mediaWiki ); \ No newline at end of file +} )( jQuery, mediaWiki ); diff --git a/specials/SpecialPageStatistics.php b/specials/SpecialPageStatistics.php index f611b0a..a7a6186 100644 --- a/specials/SpecialPageStatistics.php +++ b/specials/SpecialPageStatistics.php @@ -39,7 +39,7 @@ public function execute( $parser = null ) { $unReviewTimestamp = $wgRequest->getVal( 'unreview' ); if ( $unReviewTimestamp ) { - $rh = new ReviewHandler( $wgUser, $this->mTitle ); + $rh = new ReviewHandler( $wgUser, $this->mTitle, $wgRequest ); $rh->resetNotificationTimestamp( $unReviewTimestamp ); $wgOut->addModuleStyles( [ 'ext.watchanalytics.reviewhandler.styles' ] ); $wgOut->addHTML( $this->unReviewMessage() ); diff --git a/specials/SpecialPendingReviews.php b/specials/SpecialPendingReviews.php index e8cdc0f..5daa358 100644 --- a/specials/SpecialPendingReviews.php +++ b/specials/SpecialPendingReviews.php @@ -91,7 +91,7 @@ public function __construct() { * @return bool */ public function execute( $parser = null ) { - global $wgOut, $wgUser; + global $wgOut; $this->setHeaders(); @@ -125,27 +125,57 @@ 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( $this->mUser, $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 .= '
'; + 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, $rowCount ); + } + + $rowCount++; + } + $html .= '
'; + + } + + } + global $egPendingReviewsShowWatchSuggestionsIfReviewsUnder; // FIXME: crazy long name... if ( $rowCount < $egPendingReviewsShowWatchSuggestionsIfReviewsUnder ) { $watchSuggest = new WatchSuggest( $this->mUser ); @@ -173,10 +203,10 @@ public function handleClearNotification( $clearNotifyTitle ) { $clearNotifyTitle->getFullText(), Xml::tags( 'a', [ - 'href' => $this->getTitle()->getLocalUrl(), + 'href' => $this->getPageTitle()->getLocalUrl(), 'style' => 'font-weight:bold;', ], - $this->getTitle() + $this->getPageTitle() ) )->text() ); @@ -264,8 +294,34 @@ public function getClearNotificationTitle() { * @return string HTML for row */ public function getStandardChangeRow( PendingReview $item, $rowCount ) { + global $egPendingReviewMaxDiffRows, $egPendingReviewMaxDiffChar; + $combinedList = $this->combineLogAndChanges( $item->log, $item->newRevisions, $item->title ); $changes = $this->getPendingReviewChangesList( $combinedList ); + $acceptChangesButton = null; + + if ( count( $item->newRevisions ) ) { + $previousViewedChange = Revision::newFromRow( $item->newRevisions[0] )->getPrevious(); + if ( $previousViewedChange ) { + $prevId = $previousViewedChange->getId(); + $context = new DerivativeContext( RequestContext::getMain() ); + $context->setTitle( $item->title ); + $diff = new DifferenceEngine( $context, $prevId, 0 ); + $diff->showDiffStyle(); + $theDiff = $diff->getDiff( 'Last seen', 'Current' ); + + $numChars = strlen( $theDiff ); + $numRows = substr_count( $theDiff, 'getAcceptChangeButton( $item ); + } + + } + } if ( $item->title->isRedirect() ) { $reviewButton = $this->getAcceptRedirectButton( $item ); @@ -277,7 +333,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, $acceptChangesButton, $changes ); } /** @@ -319,7 +375,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, null, $changes ); + } + + /** + * Generates row for a pending ApprovedRevs revision. + * + * @param PendingReview $item + * @param int $rowCount used to determine if the row is odd or even + * @return string HTML for row + */ + public function getApprovedRevsChangeRow( PendingReview $item, $rowCount ) { + $changes = ''; + + $buttonOne = ''; + + $historyButton = $this->getApproveButton( $item ); + + $approvedRevID = ApprovedRevs::getApprovedRevID( $item->title ); + + $displayTitle = '' . + ' ' . + $item->title->getFullText() . + ''; + + return $this->getApproveRowHTML( $item, $rowCount, $displayTitle, $buttonOne, $historyButton, $changes ); } /** @@ -330,10 +410,11 @@ public function getDeletedPageRow( PendingReview $item, $rowCount ) { * @param string $displayTitle * @param string $buttonOne * @param string $buttonTwo + * @param string $acceptButton * @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, $acceptButton, $changes ) { // FIXME: wow this is ugly $rowClass = ( $rowCount % 2 === 0 ) ? 'pendingreviews-even-row' : 'pendingreviews-odd-row'; @@ -352,6 +433,24 @@ public function getRowHTML( PendingReview $item, $rowCount, $displayTitle, $butt "$reviewCriticalityClass pendingreviews-row-$rowCount' " . "pendingreviews-row-count='$rowCount'"; + $html = "" . + "$displayTitle" . + "" . + "$acceptButton $buttonOne $buttonTwo"; + + $html .= "$changes"; + + return $html; + } + + public function getApproveRowHTML( PendingReview $item, $rowCount, $displayTitle, $buttonOne, $buttonTwo, $changes ) { + // FIXME: wow this is ugly + $rowClass = ( $rowCount % 2 === 0 ) ? 'pendingreviews-even-row' : 'pendingreviews-odd-row'; + + $classAndAttr = "class='pendingreviews-row $rowClass " . + "ext-watchanalytics-approvable-page pendingreviews-row-$rowCount' " . + "pendingreviews-row-count='$rowCount'"; + $html = "" . "$displayTitle" . "" . @@ -407,6 +506,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. * @@ -446,7 +567,7 @@ public function getHistoryButton( $item ) { public function getClearNotificationButton( $titleText, $namespace, $buttonMsg, $buttonClass ) { return Xml::element( 'a', [ - 'href' => $this->getTitle()->getLocalURL( [ + 'href' => $this->getPageTitle()->getLocalURL( [ 'clearNotificationTitle' => $titleText, 'clearNotificationNS' => $namespace, ] ), @@ -509,6 +630,24 @@ public function getAcceptRedirectButton( $item ) { ); } + /** + * Creates a button which marks page as reviews. Displayed when diff is + * small enough to display in Special:PendingReviews. + * + * @param PendingReview $item + * + * @return string HTML for button + */ + public function getAcceptChangeButton( $item ) { + $titleText = $item->title->getDBkey(); + $namespace = $item->title->getNamespace(); + + return $this->getClearNotificationButton( + $titleText, $namespace, 'pendingreviews-accept-change', + 'pendingreviews-green-button pendingreviews-accept-change' + ); + } + /** * Creates a button bringing user to the talk page of the user who deleted * the page, allowing them to ask questions about why the page was deleted. @@ -545,9 +684,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,20 +698,11 @@ 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(); - $viewingUser = ''; - // if ( $this->mUser ) { - // $viewingUser = '&user='.$this->mUser; - // } + $viewingUser = '&user=' . $this->mUser; $linkClass = "pendingreviews-nav-link"; if ( $this->reviewOffset == 0 ) { @@ -603,6 +734,22 @@ public function getPageHeader( User $user ) { wfMessage( 'watchanalytics-pendingreviews-next-revisions' )->text() ); + $html .= '

'; + + if ( !( $this->getRequest()->getVal( 'user' ) ) ) { + if ( $numPendingReviews != 0 ) { + // message like "You have X pending reviews" + $html .= wfMessage( 'pendingreviews-num-reviews', $numPendingReviews )->text(); + } else { + // message like "Congrats you finished your reviews!" + $html .= wfMessage( 'pendingreviews-num-reviews-complete' )->text(); + } + } else { + $html .= wfMessage( 'pendingreviews-num-other-user-reviews', $user, $numPendingReviews )->text(); + } + + $html .= '

'; + return $html; } @@ -623,7 +770,7 @@ public function getPendingReviewsLegend() { $scoreThreshold )->text(); - $html .= "$msg"; + $html .= "$msg"; } // bottom threshold will always be "danger" class @@ -637,7 +784,7 @@ public function getPendingReviewsLegend() { $msg = $this->msg( "pendingreviews-reviewer-criticality-danger", $smallestThreshold - 1 )->text(); } - $html .= "$msg"; + $html .= "$msg"; $html .= '';