diff --git a/resources/sql/autopatches/20150420.imagemacro.1.sql b/resources/sql/autopatches/20150420.imagemacro.1.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20150420.imagemacro.1.sql @@ -0,0 +1,8 @@ +CREATE TABLE {$NAMESPACE}_file.file_imagemacrousage ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + macroPHID VARBINARY(64) NOT NULL, + authorPHID VARBINARY(64) NOT NULL, + count INT UNSIGNED NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20150420.imagemacro.2.backfill.php b/resources/sql/autopatches/20150420.imagemacro.2.backfill.php new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20150420.imagemacro.2.backfill.php @@ -0,0 +1,38 @@ +setConfig('viewer', PhabricatorUser::getOmnipotentUser()); +PhabricatorEnv::setRequestBaseURI('/'); // Doesn't matter during backfill + +$macro_usage_table = new PhabricatorFileImageMacroUsage(); +$macro_usage_key = PhabricatorImageMacroRemarkupRule::KEY_MACRO_USAGE; + +$xaction_classes = id(new PhutilSymbolLoader()) + ->setAncestorClass('PhabricatorApplicationTransaction') + ->loadObjects(); + +foreach ($xaction_classes as $xaction_class) { + /*if (get_class($xaction_class) != "DifferentialTransaction") { + continue; + }*/ + $objs = $xaction_class->loadAll(); + // foreach (new LiskMigrationIterator($xaction_class) as $xaction) { + foreach ($objs as $obj) { + // $obj = $xaction_class->load($xaction->getID()); + try { + $blocks = $obj->getRemarkupBlocks(); + } catch (Exception $e) { + continue; + } + // $authorPHID = $xaction->getAuthorPHID(); + $authorPHID = ''; + + foreach ($blocks as $block) { + $engine->markupText($block); + $macro_usage = $engine->getTextMetadata($macro_usage_key, array()); + //$macro_usage_table->updateUsage($authorPHID, $macro_usage); + print($authorPHID . "\n"); + print_r($macro_usage); + } + } +} diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1832,6 +1832,7 @@ 'PhabricatorFileFilePHIDType' => 'applications/files/phid/PhabricatorFileFilePHIDType.php', 'PhabricatorFileHasObjectEdgeType' => 'applications/files/edge/PhabricatorFileHasObjectEdgeType.php', 'PhabricatorFileImageMacro' => 'applications/macro/storage/PhabricatorFileImageMacro.php', + 'PhabricatorFileImageMacroUsage' => 'applications/macro/storage/PhabricatorFileImageMacroUsage.php', 'PhabricatorFileInfoController' => 'applications/files/controller/PhabricatorFileInfoController.php', 'PhabricatorFileLinkListView' => 'view/layout/PhabricatorFileLinkListView.php', 'PhabricatorFileLinkView' => 'view/layout/PhabricatorFileLinkView.php', @@ -5194,6 +5195,7 @@ 'PhabricatorFlaggableInterface', 'PhabricatorPolicyInterface', ), + 'PhabricatorFileImageMacroUsage' => 'PhabricatorFileDAO', 'PhabricatorFileInfoController' => 'PhabricatorFileController', 'PhabricatorFileLinkListView' => 'AphrontView', 'PhabricatorFileLinkView' => 'AphrontView', diff --git a/src/applications/macro/markup/PhabricatorImageMacroRemarkupRule.php b/src/applications/macro/markup/PhabricatorImageMacroRemarkupRule.php --- a/src/applications/macro/markup/PhabricatorImageMacroRemarkupRule.php +++ b/src/applications/macro/markup/PhabricatorImageMacroRemarkupRule.php @@ -5,6 +5,7 @@ private $macros; const KEY_RULE_MACRO = 'rule.macro'; + const KEY_MACRO_USAGE = 'rule.macro_usage'; public function apply($text) { return preg_replace_callback( @@ -151,6 +152,17 @@ $engine->overwriteStoredText($spec['token'], $result); } + $macro_usage_key = self::KEY_MACRO_USAGE; + $macro_usage = $engine->getTextMetadata($macro_usage_key, array()); + foreach ($metadata as $spec) { + if (isset($macro_usage[$spec['phid']])) { + $macro_usage[$spec['phid']]++; + } else { + $macro_usage[$spec['phid']] = 1; + } + } + $engine->setTextMetadata($macro_usage_key, $macro_usage); + $engine->setTextMetadata($metadata_key, array()); } diff --git a/src/applications/macro/markup/PhabricatorMemeRemarkupRule.php b/src/applications/macro/markup/PhabricatorMemeRemarkupRule.php --- a/src/applications/macro/markup/PhabricatorMemeRemarkupRule.php +++ b/src/applications/macro/markup/PhabricatorMemeRemarkupRule.php @@ -59,7 +59,26 @@ )); } - return $this->getEngine()->storeText($img); + $engine = $this->getEngine(); + + $macro_usage_key = PhabricatorImageMacroRemarkupRule::KEY_MACRO_USAGE; + $macro_usage = $engine->getTextMetadata($macro_usage_key, array()); + $viewer = $engine->getConfig('viewer'); + $macro = id(new PhabricatorMacroQuery()) + ->setViewer($viewer) + ->withNames(array($options['src'])) + ->needFiles(false) + ->executeOne(); + if ($macro) { + if (isset($macro_usage[$macro->getPHID()])) { + $macro_usage[$macro->getPHID()]++; + } else { + $macro_usage[$macro->getPHID()] = 1; + } + $engine->setTextMetadata($macro_usage_key, $macro_usage); + } + + return $engine->storeText($img); } } diff --git a/src/applications/macro/query/PhabricatorMacroQuery.php b/src/applications/macro/query/PhabricatorMacroQuery.php --- a/src/applications/macro/query/PhabricatorMacroQuery.php +++ b/src/applications/macro/query/PhabricatorMacroQuery.php @@ -3,9 +3,11 @@ final class PhabricatorMacroQuery extends PhabricatorCursorPagedPolicyAwareQuery { + private $byPopularity; private $ids; private $phids; private $authors; + private $abusers; private $names; private $nameLike; private $namePrefix; @@ -41,6 +43,11 @@ return $options; } + public function withPopularitySort($use_popularity_sort) { + $this->byPopularity = $use_popularity_sort; + return $this; + } + public function withIDs(array $ids) { $this->ids = $ids; return $this; @@ -56,6 +63,11 @@ return $this; } + public function withAbuserPHIDs(array $abusers) { + $this->abusers = $abusers; + return $this; + } + public function withNameLike($name) { $this->nameLike = $name; return $this; @@ -100,15 +112,77 @@ $macro_table = new PhabricatorFileImageMacro(); $conn = $macro_table->establishConnection('r'); - $rows = queryfx_all( + if ($this->byPopularity) { + $rows = queryfx_all( + $conn, + 'SELECT m.*, SUM(n.count) as popularity FROM %T m %Q %Q %Q %Q %Q %Q', + $macro_table->getTableName(), + $this->buildJoinClause($conn), + $this->buildWhereClause($conn), + $this->buildGroupClause($conn), + $this->buildHavingClause($conn), + $this->buildOrderClause($conn), + $this->buildLimitClause($conn)); + } else { + $rows = queryfx_all( + $conn, + 'SELECT m.* FROM %T m %Q %Q %Q', + $macro_table->getTableName(), + $this->buildWhereClause($conn), + $this->buildOrderClause($conn), + $this->buildLimitClause($conn)); + } + + $macros = $macro_table->loadAllFromArray($rows); + if ($this->byPopularity) { + $macros_by_id = mpull($macros, null, 'getId'); + + $macro_usage_table = new PhabricatorFileImageMacroUsage(); + $author_popularity_rows = queryfx_all( + $conn, + 'SELECT macroPHID, authorPHID, SUM(count) as authorUses FROM %T + GROUP BY macroPHID, authorPHID + ORDER BY macroPHID, authorUses', + $macro_usage_table->getTableName()); + $by_macro = ipull($author_popularity_rows, null, 'macroPHID'); + + foreach ($rows as $row) { + $macro_id = $row['id']; + $macro_phid = $row['phid']; + $author_phid = isset($by_macro[$macro_phid]) ? + $by_macro[$macro_phid]['authorPHID'] : null; + $author_uses = isset($by_macro[$macro_phid]) ? + $by_macro[$macro_phid]['authorUses'] : null; + $macros_by_id[$macro_id]->setPopularity( + $row['popularity'] != null ? $row['popularity'] : 0, + $author_phid, + $author_uses); + } + } + return $macros; + } + + + protected function buildHavingClause(AphrontDatabaseConnection $conn) { + $where = $this->buildPagingClause($conn); + if ($where != null) { + return 'HAVING '.$where; + } + return ''; + } + + protected function buildJoinClause(AphrontDatabaseConnection $conn) { + assert($this->byPopularity); + $macro_usage_table = new PhabricatorFileImageMacroUsage(); + + $joins = array(); + $joins[] = qsprintf( $conn, - 'SELECT m.* FROM %T m %Q %Q %Q', - $macro_table->getTableName(), - $this->buildWhereClause($conn), - $this->buildOrderClause($conn), - $this->buildLimitClause($conn)); + 'LEFT JOIN %T n ON m.phid = n.macroPHID', + $macro_usage_table->getTableName()); + + return implode(' ', $joins); - return $macro_table->loadAllFromArray($rows); } protected function buildWhereClause(AphrontDatabaseConnection $conn) { @@ -135,6 +209,13 @@ $this->authors); } + if ($this->abusers && $this->byPopularity) { + $where[] = qsprintf( + $conn, + 'n.authorPHID IN (%Ls)', + $this->abusers); + } + if ($this->nameLike) { $where[] = qsprintf( $conn, @@ -210,11 +291,22 @@ } } - $where[] = $this->buildPagingClause($conn); + if (!$this->byPopularity) { + $where[] = $this->buildPagingClause($conn); + } return $this->formatWhereClause($where); } + protected function buildGroupClause(AphrontDatabaseConnection $conn) { + $group = array(); + $group[] = qsprintf( + $conn, + 'GROUP BY m.phid'); + + return implode(' ', $group); + } + protected function didFilterPage(array $macros) { if ($this->needFiles) { $file_phids = mpull($macros, 'getFilePHID'); @@ -242,6 +334,53 @@ return 'm'; } + protected function getPagingColumn() { + $popularity_order = 'popularity '. + ($this->getBeforeID() ? 'ASC' : 'DESC').', m.id'; + return $this->byPopularity ? $popularity_order : 'm.id'; + } + + protected function getPagingValue($macro) { + if ($this->byPopularity) { + $popularity = $macro->getPopularity(); + return (string)$popularity['popularity'].','.(string)$macro->getId(); + } + return PhabricatorCursorPagedPolicyAwareQuery::getPagingValue($macro); + } + + protected function buildPagingClause( + AphrontDatabaseConnection $conn_r) { + + if ($this->byPopularity) { + if ($this->getBeforeID()) { + $strs = explode(',', $this->getBeforeID()); + $beforePopularity = (int)$strs[0]; + $beforeId = (int)$strs[1]; + + return qsprintf( + $conn_r, + 'popularity > %d OR (popularity = %d AND m.id > %d)', + $beforePopularity, + $beforePopularity, + $beforeId); + } else if ($this->getAfterID()) { + $strs = explode(',', $this->getAfterID()); + $afterPopularity = (int)$strs[0]; + $afterId = (int)$strs[1]; + + return qsprintf( + $conn_r, + 'popularity < %d OR (popularity = %d AND m.id < %d)', + $afterPopularity, + $afterPopularity, + $afterId); + } + + return null; + } + return PhabricatorCursorPagedPolicyAwareQuery::buildPagingClause($conn_r); + } + public function getQueryApplicationClass() { return 'PhabricatorMacroApplication'; } diff --git a/src/applications/macro/query/PhabricatorMacroSearchEngine.php b/src/applications/macro/query/PhabricatorMacroSearchEngine.php --- a/src/applications/macro/query/PhabricatorMacroSearchEngine.php +++ b/src/applications/macro/query/PhabricatorMacroSearchEngine.php @@ -17,6 +17,12 @@ 'authorPHIDs', $this->readUsersFromRequest($request, 'authors')); + $abuser_phids = $this->readUsersFromRequest($request, 'abusers'); + $saved->setParameter( + 'abuserPHIDs', + $abuser_phids); + $saved->setParameter('popularitySort', !empty($abuser_phids)); + $saved->setParameter('status', $request->getStr('status')); $saved->setParameter('names', $request->getStrList('names')); $saved->setParameter('nameLike', $request->getStr('nameLike')); @@ -34,7 +40,9 @@ ->needFiles(true) ->withIDs($saved->getParameter('ids', array())) ->withPHIDs($saved->getParameter('phids', array())) - ->withAuthorPHIDs($saved->getParameter('authorPHIDs', array())); + ->withAuthorPHIDs($saved->getParameter('authorPHIDs', array())) + ->withAbuserPHIDs($saved->getParameter('abuserPHIDs', array())) + ->withPopularitySort($saved->getParameter('popularitySort', false)); $this->setQueryOrder($query, $saved); @@ -79,6 +87,7 @@ PhabricatorSavedQuery $saved) { $author_phids = $saved->getParameter('authorPHIDs', array()); + $abuser_phids = $saved->getParameter('abuserPHIDs', array()); $status = $saved->getParameter('status'); $names = implode(', ', $saved->getParameter('names', array())); $like = $saved->getParameter('nameLike'); @@ -97,6 +106,12 @@ ->setName('authors') ->setLabel(pht('Authors')) ->setValue($author_phids)) + ->appendControl( + id(new AphrontFormTokenizerControl()) + ->setDatasource(new PhabricatorPeopleDatasource()) + ->setName('abusers') + ->setLabel(pht('Abused By')) + ->setValue($abuser_phids)) ->appendChild( id(new AphrontFormTextControl()) ->setName('nameLike') @@ -135,11 +150,13 @@ protected function getBuiltinQueryNames() { $names = array( 'active' => pht('Active'), - 'all' => pht('All'), + 'recent' => pht('Recent'), + 'popular' => pht('Popular'), ); if ($this->requireViewer()->isLoggedIn()) { $names['authored'] = pht('Authored'); + $names['favorites'] = pht('Favorites'); } return $names; @@ -152,7 +169,7 @@ switch ($query_key) { case 'active': return $query; - case 'all': + case 'recent': return $query->setParameter( 'status', PhabricatorMacroQuery::STATUS_ANY); @@ -160,6 +177,25 @@ return $query->setParameter( 'authorPHIDs', array($this->requireViewer()->getPHID())); + case 'popular': + return $query + ->setParameter( + 'status', + PhabricatorMacroQuery::STATUS_ANY) + ->setParameter( + 'popularitySort', + true); + case 'favorites': + return $query + ->setParameter( + 'status', + PhabricatorMacroQuery::STATUS_ANY) + ->setParameter( + 'popularitySort', + true) + ->setParameter( + 'abuserPHIDs', + array($this->requireViewer()->getPHID())); } return parent::buildSavedQueryFromBuiltin($query_key); @@ -168,7 +204,9 @@ protected function getRequiredHandlePHIDsForResultList( array $macros, PhabricatorSavedQuery $query) { - return mpull($macros, 'getAuthorPHID'); + return array_merge( + mpull($macros, 'getAuthorPHID'), + ipull(mpull($macros, 'getPopularity'), 'top_abuser')); } protected function renderResultList( @@ -212,6 +250,30 @@ pht('Created by %s', $author_handle->renderLink())); } + $popularity = $macro->getPopularity(); + if ($popularity != null) { + $popularizer_phid = $popularity['top_abuser']; + + $item->appendChild( + phutil_tag( + 'div', + array(), + pht('Popularity %d', $popularity['popularity']))); + + $message = $popularity['popularity'] == 0 ? + pht('Wow. This macro must suck.') : + pht('Abused most by %s (%d time%s)', + $handles[$popularizer_phid]->renderLink(), + $popularity['top_abuser_count'], + $popularity['top_abuser_count'] != 1 ? 's' : ''); + $item->appendChild( + phutil_tag( + 'div', + array(), + $message)); + } + + $item->setURI($this->getApplicationURI('/view/'.$macro->getID().'/')); $item->setDisabled($macro->getisDisabled()); $item->setHeader($macro->getName()); diff --git a/src/applications/macro/storage/PhabricatorFileImageMacro.php b/src/applications/macro/storage/PhabricatorFileImageMacro.php --- a/src/applications/macro/storage/PhabricatorFileImageMacro.php +++ b/src/applications/macro/storage/PhabricatorFileImageMacro.php @@ -17,6 +17,9 @@ private $file = self::ATTACHABLE; private $audio = self::ATTACHABLE; + private $popularity = null; + private $topAbuser = null; + private $topAbuserCount = null; const AUDIO_BEHAVIOR_NONE = 'audio:none'; const AUDIO_BEHAVIOR_ONCE = 'audio:once'; @@ -79,6 +82,23 @@ return parent::save(); } + public function setPopularity($popularity, $top_abuser, $top_abuser_count) { + $this->popularity = $popularity; + $this->topAbuser = $top_abuser; + $this->topAbuserCount = $top_abuser_count; + } + + public function getPopularity() { + if (is_null($this->popularity)) { + return null; + } else { + return array( + 'popularity' => $this->popularity, + 'top_abuser' => $this->topAbuser, + 'top_abuser_count' => $this->topAbuserCount); + } + } + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/macro/storage/PhabricatorFileImageMacroUsage.php b/src/applications/macro/storage/PhabricatorFileImageMacroUsage.php new file mode 100644 --- /dev/null +++ b/src/applications/macro/storage/PhabricatorFileImageMacroUsage.php @@ -0,0 +1,51 @@ + array( + 'macroPHID' => 'phid?', + 'authorPHID' => 'phid?', + 'count' => 'uint32', + ), + self::CONFIG_KEY_SCHEMA => array( + ), + ) + parent::getConfiguration(); + } + + public function updateUsage($author_phid, $macros) { + if (empty($macros)) { + return; + } + + $conn = $this->establishConnection('w'); + + // Append to our wonderful macrousage table + $values = array(); + foreach ($macros as $macro_phid => $count) { + $values[] = qsprintf( + $conn, + '(%s, %s, %d)', + $macro_phid, + $author_phid, + $count); + } + $values_clause = implode(',', $values); + + queryfx( + $conn, + 'INSERT INTO %T + (macroPHID, authorPHID, count) + VALUES %Q', + $this->getTableName(), + $values_clause); + } +} diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -1265,6 +1265,18 @@ $blocks, $engine); + $macro_usage_table = new PhabricatorFileImageMacroUsage(); + $macro_usage_key = PhabricatorImageMacroRemarkupRule::KEY_MACRO_USAGE; + $authorPHID = $this->getActor()->getPHID(); + + foreach ($blocks as $key => $xaction_blocks) { + foreach ($xaction_blocks as $block) { + $engine->markupText($block); + $macro_usage = $engine->getTextMetadata($macro_usage_key, array()); + $macro_usage_table->updateUsage($authorPHID, $macro_usage); + } + } + $mentioned_phids = array(); if ($this->shouldEnableMentions($object, $xactions)) { foreach ($blocks as $key => $xaction_blocks) {