Added 'mark as read' system for moderation queue items
authorMarcel Werk <burntime@woltlab.com>
Thu, 11 Dec 2014 13:30:18 +0000 (14:30 +0100)
committerMarcel Werk <burntime@woltlab.com>
Thu, 11 Dec 2014 13:30:18 +0000 (14:30 +0100)
com.woltlab.wcf/objectType.xml
com.woltlab.wcf/templates/moderationList.tpl
com.woltlab.wcf/templates/userPanel.tpl
wcfsetup/install/files/js/WCF.Moderation.js
wcfsetup/install/files/lib/data/moderation/queue/ModerationQueue.class.php
wcfsetup/install/files/lib/data/moderation/queue/ModerationQueueAction.class.php
wcfsetup/install/files/lib/data/moderation/queue/ViewableModerationQueue.class.php
wcfsetup/install/files/lib/form/AbstractModerationForm.class.php
wcfsetup/install/files/lib/system/moderation/queue/ModerationQueueManager.class.php
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml

index 4a9c9c370dbb528460f2d0d3728ab59e993f0c3e..488a260746040c2ef40069b632dcabe99eaacc1e 100644 (file)
                </type>
                <!-- /moderation type -->
                
+               <!-- Visit Tracker -->
+               <type>
+                       <name>com.woltlab.wcf.moderation.queue</name>
+                       <definitionname>com.woltlab.wcf.visitTracker.objectType</definitionname>
+               </type>
+               <!-- /Visit Tracker -->
+               
                <!-- activity points -->
                <type>
                        <name>com.woltlab.wcf.like.activityPointEvent.receivedLikes</name>
index 228e8b9f50fc8c290bc5f5842df3510417ca0c9a..7c135ac44232b1abbc8a8ac7d0a31e985cfd84cf 100644 (file)
@@ -4,6 +4,15 @@
        <title>{lang}wcf.moderation.moderation{/lang} {if $pageNo > 1}- {lang}wcf.page.pageNo{/lang} {/if}- {PAGE_TITLE|language}</title>
        
        {include file='headInclude'}
+       
+       <script data-relocate="true">
+               //<![CDATA[
+               $(function() {
+                       new WCF.Moderation.Queue.MarkAsRead();
+                       new WCF.Moderation.Queue.MarkAllAsRead();
+               });
+               //]]>
+       </script>
 </head>
 
 <body id="tpl{$templateName|ucfirst}" data-template="{$templateName}" data-application="{$templateNameApplication}">
        {event name='sidebarBoxes'}
 {/capture}
 
+{capture assign='headerNavigation'}
+       <li class="jsOnly"><a href="#" title="{lang}wcf.moderation.markAllAsRead{/lang}" class="markAllAsReadButton jsTooltip"><span class="icon icon16 icon-ok"></span> <span class="invisible">{lang}wcf.moderation.markAllAsRead{/lang}</span></a></li>
+{/capture}
+
 {include file='header' sidebarOrientation='left'}
 
 <header class="boxHeadline">
                <table class="table">
                        <thead>
                                <tr>
-                                       <th class="columnID{if $sortField == 'queueID'} active {@$sortOrder}{/if}"><a href="{link controller='ModerationList'}definitionID={@$definitionID}&assignedUserID={@$assignedUserID}&status={@$status}&pageNo={@$pageNo}&sortField=queueID&sortOrder={if $sortField == 'queueID' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.global.objectID{/lang}</a></th>
                                        <th class="columnText columnTitle" colspan="2">{lang}wcf.moderation.title{/lang}</th>
                                        <th class="columnText columnAssignedUserID{if $sortField == 'assignedUsername'} active {@$sortOrder}{/if}"><a href="{link controller='ModerationList'}definitionID={@$definitionID}&assignedUserID={@$assignedUserID}&status={@$status}&pageNo={@$pageNo}&sortField=assignedUsername&sortOrder={if $sortField == 'assignedUsername' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.moderation.assignedUser{/lang}</a></th>
                                        <th class="columnDigits columnComments{if $sortField == 'comments'} active {@$sortOrder}{/if}"><a href="{link controller='ModerationList'}definitionID={@$definitionID}&assignedUserID={@$assignedUserID}&status={@$status}&pageNo={@$pageNo}&sortField=comments&sortOrder={if $sortField == 'comments' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.moderation.comments{/lang}</a></th>
                        
                        <tbody>
                                {foreach from=$objects item=entry}
-                                       <tr>
-                                               <td class="columnID">{@$entry->queueID}</td>
-                                               <td class="columnIcon"><p class="framed">{@$entry->getUserProfile()->getAvatar()->getImageTag(32)}</p></td>
+                                       <tr class="moderationQueueEntry{if $entry->isNew()} new{/if}" data-queue-id="{@$entry->queueID}">
+                                               <td class="columnIcon columnAvatar">
+                                                       <div>
+                                                               <p class="framed"{if $entry->isNew()} title="{lang}wcf.moderation.markAsRead.doubleClick{/lang}"{/if}>{@$entry->getUserProfile()->getAvatar()->getImageTag(32)}</p>
+                                                       </div>
+                                               </td>
                                                <td class="columnText columnSubject">
                                                        <h3>
                                                                <span class="badge label">{lang}wcf.moderation.type.{@$definitionNames[$entry->objectTypeID]}{/lang}</span>
index a1adbde952e812892975675fd38bfced90af2983..684b58ebae16d0c8e236f90d1b82b80bec2a6342 100644 (file)
                        <a href="{link controller='ModerationList'}{/link}">
                                <span class="icon icon16 icon-warning-sign"></span>
                                <span>{lang}wcf.moderation.moderation{/lang}</span>
-                               {if $__wcf->getModerationQueueManager()->getOutstandingModerationCount()}<span class="badge badgeInverse">{#$__wcf->getModerationQueueManager()->getOutstandingModerationCount()}</span>{/if}
+                               {if $__wcf->getModerationQueueManager()->getUnreadModerationCount()}<span class="badge badgeInverse">{#$__wcf->getModerationQueueManager()->getUnreadModerationCount()}</span>{/if}
                        </a>
                        {if !OFFLINE || $__wcf->session->getPermission('admin.general.canViewPageDuringOfflineMode')}
                                <script data-relocate="true">
index 5675546a02dce34a9b7c875bf06e05fc97a05615..3e44f23c702bd80d0760de2a03437fcf84c37e3b 100644 (file)
@@ -269,6 +269,122 @@ WCF.Moderation.Management = Class.extend({
        }
 });
 
+/**
+ * Namespace for moderation queue related classes.
+ */
+WCF.Moderation.Queue = { };
+
+/**
+ * Marks one moderation queue entry as read.
+ */
+WCF.Moderation.Queue.MarkAsRead = Class.extend({
+       /**
+        * action proxy
+        * @var WCF.Action.Proxy
+        */
+       _proxy: null,
+       
+       /**
+        * Initializes the mark as read for queue entries.
+        */
+       init: function() {
+               this._proxy = new WCF.Action.Proxy({
+                       success: $.proxy(this._success, this)
+               });
+               
+               $(document).on('dblclick', '.moderationList .new .columnAvatar', $.proxy(this._dblclick, this));
+       },
+       
+       /**
+        * Handles double clicks on avatar.
+        * 
+        * @param       object          event
+        */
+       _dblclick: function(event) {
+               this._proxy.setOption('data', {
+                       actionName: 'markAsRead',
+                       className: 'wcf\\data\\moderation\\queue\\ModerationQueueAction',
+                       objectIDs: [ $(event.currentTarget).parents('tr:eq(0)').data('queueID') ]
+               });
+               this._proxy.sendRequest();
+       },
+       
+       /**
+        * Handles successful AJAX requests.
+        * 
+        * @param       object          data
+        * @param       string          textStatus
+        * @param       jQuery          jqXHR
+        */
+       _success: function(data, textStatus, jqXHR) {
+               $('.moderationList .new').each(function(index, element) {
+                       var $element = $(element);
+                       if (WCF.inArray($element.data('queueID'), data.objectIDs)) {
+                               // remove new class
+                               $element.removeClass('new');
+                               
+                               // remove event
+                               $element.find('.columnAvatar').off('dblclick');
+                       }
+               });
+       }
+});
+
+/**
+ * Marks all moderation queue entries as read.
+ */
+WCF.Moderation.Queue.MarkAllAsRead = Class.extend({
+       /**
+        * action proxy
+        * @var WCF.Action.Proxy
+        */
+       _proxy: null,
+       
+       /**
+        * Initializes the WCF.Moderation.Queue.MarkAllAsRead class.
+        */
+       init: function() {
+               this._proxy = new WCF.Action.Proxy({
+                       success: $.proxy(this._success, this)
+               });
+               
+               $('.markAllAsReadButton').click($.proxy(this._click, this));
+       },
+       
+       /**
+        * Handles clicks.
+        * 
+        * @param       object          event
+        */
+       _click: function(event) {
+               event.preventDefault();
+               
+               this._proxy.setOption('data', {
+                       actionName: 'markAllAsRead',
+                       className: 'wcf\\data\\moderation\\queue\\ModerationQueueAction'
+               });
+               this._proxy.sendRequest();
+       },
+       
+       /**
+        * Marks all queue entries as read.
+        * 
+        * @param       object          data
+        * @param       string          textStatus
+        * @param       jQuery          jqXHR
+        */
+       _success: function(data, textStatus, jqXHR) {
+               // @todo fix dropdown
+               
+               // @todo remove badge in userpanel
+                               
+               // fix moderation list
+               var $moderationList = $('.moderationList');
+               $moderationList.find('.new').removeClass('new');
+               $moderationList.find('.columnAvatar').off('dblclick');
+       }
+});
+
 /**
  * Namespace for activation related classes.
  */
index ee898480aa4d083493b455f09ab501c7ce12bdf0..08bed4d2434040f3927d078146797721cb83d540 100644 (file)
@@ -86,7 +86,7 @@ class ModerationQueue extends DatabaseObject {
         * @return      boolean
         */
        public function isDone() {
-               return ($this->status == self::STATUS_DONE);
+               return ($this->status == self::STATUS_DONE || $this->status == self::STATUS_CONFIRMED || $this->status == self::STATUS_REJECTED);
        }
        
        /**
index ab8ef634fe4446d8583f66878935ebb5ca06b0f7..ce62822e70f0e7e069297fe361fdb2a612dec9e2 100644 (file)
@@ -9,6 +9,7 @@ use wcf\system\exception\UserInputException;
 use wcf\system\moderation\queue\ModerationQueueManager;
 use wcf\system\request\LinkHandler;
 use wcf\system\user\storage\UserStorageHandler;
+use wcf\system\visitTracker\VisitTracker;
 use wcf\system\WCF;
 
 /**
@@ -237,4 +238,56 @@ class ModerationQueueAction extends AbstractDatabaseObjectAction {
                        'username' => $username
                );
        }
+       
+       /**
+        * Marks queue entries as read.
+        */
+       public function markAsRead() {
+               if (empty($this->parameters['visitTime'])) {
+                       $this->parameters['visitTime'] = TIME_NOW;
+               }
+       
+               if (empty($this->objects)) {
+                       $this->readObjects();
+               }
+       
+               foreach ($this->objects as $queue) {
+                       VisitTracker::getInstance()->trackObjectVisit('com.woltlab.wcf.moderation.queue', $queue->queueID, $this->parameters['visitTime']);
+               }
+       
+               // reset storage
+               UserStorageHandler::getInstance()->reset(array(WCF::getUser()->userID), 'unreadModerationCount');
+       }
+       
+       /**
+        * @see \wcf\data\IVisitableObjectAction::validateMarkAsRead()
+        */
+       public function validateMarkAsRead() {
+               if (empty($this->objects)) {
+                       $this->readObjects();
+               }
+               
+               foreach ($this->objects as $queue) {
+                       if (!$queue->canEdit()) {
+                               throw new PermissionDeniedException();
+                       }
+               }
+       }
+       
+       /**
+        * Marks all queue entries as read.
+        */
+       public function markAllAsRead() {
+               VisitTracker::getInstance()->trackTypeVisit('com.woltlab.wcf.moderation.queue');
+               
+               // reset storage
+               UserStorageHandler::getInstance()->reset(array(WCF::getUser()->userID), 'unreadModerationCount');
+       }
+       
+       /**
+        * Validates the mark all as read action.
+        */
+       public function validateMarkAllAsRead() {
+               // does nothing
+       }
 }
index e1ce9b3d2fb702035807a71f49a8219a04c2bfd2..30af9b7f385b15386dce597ccbe8fb7c757d82e8 100644 (file)
@@ -6,6 +6,7 @@ use wcf\data\user\UserProfile;
 use wcf\data\DatabaseObjectDecorator;
 use wcf\data\IUserContent;
 use wcf\system\moderation\queue\ModerationQueueManager;
+use wcf\system\visitTracker\VisitTracker;
 
 /**
  * Represents a viewable moderation queue entry.
@@ -163,4 +164,15 @@ class ViewableModerationQueue extends DatabaseObjectDecorator {
                $objectType = ObjectTypeCache::getInstance()->getObjectType($this->objectTypeID);
                return $objectType->objectType;
        }
+       
+       /**
+        * Returns true if this queue item is new for the active user.
+        *
+        * @return      boolean
+        */
+       public function isNew() {
+               if ($this->time > max(VisitTracker::getInstance()->getVisitTime('com.woltlab.wcf.moderation.queue'), VisitTracker::getInstance()->getObjectVisitTime('com.woltlab.wcf.moderation.queue', $this->queueID))) return true;
+       
+               return false;
+       }
 }
index 6ffc252ea0c5f14ff97265d1145b096b345930dd..3822aac7d7106dd2f31a7ce947d2c1d019469bdc 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 namespace wcf\form;
+use wcf\data\moderation\queue\ModerationQueueAction;
 use wcf\data\moderation\queue\ViewableModerationQueue;
 use wcf\system\breadcrumb\Breadcrumb;
 use wcf\system\comment\CommentHandler;
@@ -102,6 +103,14 @@ abstract class AbstractModerationForm extends AbstractForm {
                $this->commentObjectTypeID = CommentHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.moderation.queue');
                $this->commentManager = CommentHandler::getInstance()->getObjectType($this->commentObjectTypeID)->getProcessor();
                $this->commentList = CommentHandler::getInstance()->getCommentList($this->commentManager, $this->commentObjectTypeID, $this->queueID);
+               
+               // update queue visit
+               if ($this->queue->isNew()) {
+                       $action = new ModerationQueueAction(array($this->queue->getDecoratedObject()), 'markAsRead', array(
+                               'visitTime' => TIME_NOW
+                       ));
+                       $action->executeAction();
+               }
        }
        
        /**
index 4864e93bdf3b7257385c75cee0f0019036233b2f..f9b75ddb530364cede03dee6be3e5c25c3b9cdf0 100644 (file)
@@ -6,6 +6,7 @@ use wcf\data\object\type\ObjectTypeCache;
 use wcf\system\database\util\PreparedStatementConditionBuilder;
 use wcf\system\exception\SystemException;
 use wcf\system\user\storage\UserStorageHandler;
+use wcf\system\visitTracker\VisitTracker;
 use wcf\system\SingletonFactory;
 use wcf\system\WCF;
 
@@ -214,29 +215,7 @@ class ModerationQueueManager extends SingletonFactory {
                // cache does not exist or is outdated
                if ($count === null) {
                        // force update of non-tracked queues for this user
-                       $queueList = new ModerationQueueList();
-                       $queueList->sqlJoins = "LEFT JOIN wcf".WCF_N."_moderation_queue_to_user moderation_queue_to_user ON (moderation_queue_to_user.queueID = moderation_queue.queueID AND moderation_queue_to_user.userID = ".WCF::getUser()->userID.")";
-                       $queueList->getConditionBuilder()->add("moderation_queue_to_user.queueID IS NULL");
-                       $queueList->readObjects();
-                       
-                       if (count($queueList)) {
-                               $queues = array();
-                               foreach ($queueList as $queue) {
-                                       if (!isset($queues[$queue->objectTypeID])) {
-                                               $queues[$queue->objectTypeID] = array();
-                                       }
-                                       
-                                       $queues[$queue->objectTypeID][$queue->queueID] = $queue;
-                               }
-                               
-                               foreach ($this->objectTypeNames as $definitionName => $objectTypeIDs) {
-                                       foreach ($objectTypeIDs as $objectTypeID) {
-                                               if (isset($queues[$objectTypeID])) {
-                                                       $this->moderationTypes[$definitionName]->getProcessor()->assignQueues($objectTypeID, $queues[$objectTypeID]);
-                                               }
-                                       }
-                               }
-                       }
+                       $this->forceUserAssignment();
                        
                        // count outstanding and assigned queues
                        $conditions = new PreparedStatementConditionBuilder();
@@ -261,6 +240,76 @@ class ModerationQueueManager extends SingletonFactory {
                return $count;
        }
        
+       /**
+        * Returns the count of unread moderation queue items.
+        *
+        * @return      integer
+        */
+       public function getUnreadModerationCount() {
+               // get count
+               $count = UserStorageHandler::getInstance()->getField('unreadModerationCount');
+       
+               // cache does not exist or is outdated
+               if ($count === null) {
+                       // force update of non-tracked queues for this user
+                       $this->forceUserAssignment();
+                       
+                       // count outstanding and assigned queues
+                       $conditions = new PreparedStatementConditionBuilder();
+                       $conditions->add("moderation_queue_to_user.userID = ?", array(WCF::getUser()->userID));
+                       $conditions->add("moderation_queue_to_user.isAffected = ?", array(1));
+                       $conditions->add("moderation_queue.status IN (?)", array(array(ModerationQueue::STATUS_OUTSTANDING, ModerationQueue::STATUS_PROCESSING)));
+                       $conditions->add("moderation_queue.time > ?", array(VisitTracker::getInstance()->getVisitTime('com.woltlab.wcf.moderation.queue')));
+                       $conditions->add("(moderation_queue.time > tracked_visit.visitTime OR tracked_visit.visitTime IS NULL)");
+                       
+                       $sql = "SELECT          COUNT(*) AS count
+                               FROM            wcf".WCF_N."_moderation_queue_to_user moderation_queue_to_user
+                               LEFT JOIN       wcf".WCF_N."_moderation_queue moderation_queue
+                               ON              (moderation_queue.queueID = moderation_queue_to_user.queueID)
+                               LEFT JOIN       wcf".WCF_N."_tracked_visit tracked_visit
+                               ON              (tracked_visit.objectTypeID = ".VisitTracker::getInstance()->getObjectTypeID('com.woltlab.wcf.moderation.queue')." AND tracked_visit.objectID = moderation_queue.queueID AND tracked_visit.userID = ".WCF::getUser()->userID.")
+                               ".$conditions;
+                       $statement = WCF::getDB()->prepareStatement($sql);
+                       $statement->execute($conditions->getParameters());
+                       $row = $statement->fetchArray();
+                       $count = $row['count'];
+                               
+                       // update storage data
+                       UserStorageHandler::getInstance()->update(WCF::getUser()->userID, 'unreadModerationCount', $count);
+               }
+       
+               return $count;
+       }
+       
+       /**
+        * Forces the update of non-tracked queues for this user.
+        */
+       protected function forceUserAssignment() {
+               $queueList = new ModerationQueueList();
+               $queueList->sqlJoins = "LEFT JOIN wcf".WCF_N."_moderation_queue_to_user moderation_queue_to_user ON (moderation_queue_to_user.queueID = moderation_queue.queueID AND moderation_queue_to_user.userID = ".WCF::getUser()->userID.")";
+               $queueList->getConditionBuilder()->add("moderation_queue_to_user.queueID IS NULL");
+               $queueList->readObjects();
+                       
+               if (count($queueList)) {
+                       $queues = array();
+                       foreach ($queueList as $queue) {
+                               if (!isset($queues[$queue->objectTypeID])) {
+                                       $queues[$queue->objectTypeID] = array();
+                               }
+                                       
+                               $queues[$queue->objectTypeID][$queue->queueID] = $queue;
+                       }
+               
+                       foreach ($this->objectTypeNames as $definitionName => $objectTypeIDs) {
+                               foreach ($objectTypeIDs as $objectTypeID) {
+                                       if (isset($queues[$objectTypeID])) {
+                                               $this->moderationTypes[$definitionName]->getProcessor()->assignQueues($objectTypeID, $queues[$objectTypeID]);
+                                       }
+                               }
+                       }
+               }
+       }
+       
        /**
         * Saves moderation queue assignments.
         * 
@@ -347,9 +396,11 @@ class ModerationQueueManager extends SingletonFactory {
        public function resetModerationCount($userID = null) {
                if ($userID === null) {
                        UserStorageHandler::getInstance()->resetAll('outstandingModerationCount');
+                       UserStorageHandler::getInstance()->resetAll('unreadModerationCount');
                }
                else {
                        UserStorageHandler::getInstance()->reset(array($userID), 'outstandingModerationCount');
+                       UserStorageHandler::getInstance()->reset(array($userID), 'unreadModerationCount');
                }
        }
        
index 644a1d5874dbae0a8c952603db5aceb4343e9081..6062de81c5f537860b3cd1e35d4452bff75ed3fd 100644 (file)
@@ -2348,6 +2348,8 @@ Fehler sind beispielsweise:
                <item name="wcf.moderation.comments"><![CDATA[Kommentare]]></item>
                <item name="wcf.moderation.comments.description"><![CDATA[Diese Kommentare sind intern und nur für Moderatoren einsehbar]]></item>
                <item name="wcf.moderation.jumpToContent"><![CDATA[Zum Inhalt gehen]]></item>
+               <item name="wcf.moderation.markAllAsRead"><![CDATA[Alle Einträge als gelesen markieren]]></item>
+               <item name="wcf.moderation.markAsRead.doubleClick"><![CDATA[Eintrag durch Doppelklick als gelesen markieren]]></item>
        </category>
        
        <category name="wcf.moderation.activation">
index 790195d9c39e51dfb75055082b1209a61d2e099b..dbc3661fce9c1fa918f7c9ddb2f3a73a3c832a2b 100644 (file)
@@ -2347,6 +2347,8 @@ Errors are:
                <item name="wcf.moderation.comments"><![CDATA[Comments]]></item>
                <item name="wcf.moderation.comments.description"><![CDATA[All comments are internal and will not be exposed to non-moderators]]></item>
                <item name="wcf.moderation.jumpToContent"><![CDATA[Go to Related Content]]></item>
+               <item name="wcf.moderation.markAllAsRead"><![CDATA[Mark All Items Read]]></item>
+               <item name="wcf.moderation.markAsRead.doubleClick"><![CDATA[Double-Click to Mark This Item Read]]></item>
        </category>
        
        <category name="wcf.moderation.activation">