Bsitu has uploaded a new change for review. https://gerrit.wikimedia.org/r/134982
Change subject: [WIP]Flow revision multi-state support ...................................................................... [WIP]Flow revision multi-state support DON'T REVIEW, this is a very initial draft The ultimate goal is to support revision delete. Move delete/suppress action from board action menu to history page. hide/close actions are kept in flow history. delete/suppress will be removed from flow history and logged in general logging table This is step one of the followings: * Add multi-state support to collect data to new table * Maintenance script to populate data to new table for old records * Have flow code reference the new table for revision state * Drop old data columns Change-Id: I269b5cd143ea956b670d46483486b3cecaeebcd7 --- M Flow.php M Hooks.php A db_patches/patch-flow_revision_state.sql M flow.sql M includes/Data/RevisionStorage.php M includes/Model/AbstractRevision.php A includes/Model/RevisionState.php 7 files changed, 361 insertions(+), 2 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Flow refs/changes/82/134982/1 diff --git a/Flow.php b/Flow.php index effc0a0..d3e4ca4 100755 --- a/Flow.php +++ b/Flow.php @@ -97,6 +97,7 @@ $wgAutoloadClasses['Flow\Model\PostSummary'] = $dir . 'includes/Model/PostSummary.php'; $wgAutoloadClasses['Flow\Model\TopicListEntry'] = $dir . 'includes/Model/TopicListEntry.php'; $wgAutoloadClasses['Flow\Model\Workflow'] = $dir . 'includes/Model/Workflow.php'; +$wgAutoloadClasses['Flow\Model\RevisionState'] = $dir . 'includes/Model/RevisionState.php'; $wgAutoloadClasses['Flow\Model\UUID'] = "$dir/includes/Model/UUID.php"; $wgAutoloadClasses['Flow\Collection\AbstractCollection'] = $dir . 'includes/Collection/AbstractCollection.php'; $wgAutoloadClasses['Flow\Collection\CollectionCache'] = $dir . 'includes/Collection/CollectionCache.php'; diff --git a/Hooks.php b/Hooks.php index 75d5eea..953aaf5 100644 --- a/Hooks.php +++ b/Hooks.php @@ -126,6 +126,7 @@ $updater->modifyExtensionField( 'flow_revision', 'rev_user_ip', "$dir/db_patches/patch-revision_user_ip.sql" ); $updater->addExtensionField( 'flow_revision', 'rev_type_id', "$dir/db_patches/patch-rev_type_id.sql" ); $updater->addExtensionTable( 'flow_ext_ref', "$dir/db_patches/patch-add-linkstables.sql" ); + $updater->addExtensionTable( 'flow_revision_state', "$dir/db_patches/patch-flow_revision_state.sql" ); require_once __DIR__.'/maintenance/FlowInsertDefaultDefinitions.php'; $updater->addPostDatabaseUpdateMaintenance( 'FlowInsertDefaultDefinitions' ); diff --git a/db_patches/patch-flow_revision_state.sql b/db_patches/patch-flow_revision_state.sql new file mode 100644 index 0000000..1520a1b --- /dev/null +++ b/db_patches/patch-flow_revision_state.sql @@ -0,0 +1,10 @@ +CREATE TABLE /*_*/flow_revision_state ( + frs_rev_id binary(11) not null, + frs_state varchar(32) binary not null, + frs_user_id bigint unsigned not null default 0, + frs_user_ip varbinary(39) default null, + frs_user_wiki varchar(32) binary not null default '', + frs_comment varchar(255) binary +) /*$wgDBTableOptions*/; + +CREATE UNIQUE INDEX /*i*/flow_revision_state_rev_id_state ON /*_*/flow_revision_state (frs_rev_id,frs_state); diff --git a/flow.sql b/flow.sql index 67fdbd0..0165461 100644 --- a/flow.sql +++ b/flow.sql @@ -136,6 +136,17 @@ CREATE INDEX /*i*/flow_revision_user ON /*_*/flow_revision (rev_user_id, rev_user_ip, rev_user_wiki); +CREATE TABLE /*_*/flow_revision_state ( + frs_rev_id binary(11) not null, + frs_state varchar(32) binary not null, + frs_user_id bigint unsigned not null default 0, + frs_user_ip varbinary(39) default null, + frs_user_wiki varchar(32) binary not null default '', + frs_comment varchar(255) binary +) /*$wgDBTableOptions*/; + +CREATE UNIQUE INDEX /*i*/flow_revision_state_rev_id_state ON /*_*/flow_revision_state (frs_rev_id,frs_state); + -- Closure table implementation of tree storage in sql -- We may be able to go simpler than this CREATE TABLE /*_*/flow_tree_node ( diff --git a/includes/Data/RevisionStorage.php b/includes/Data/RevisionStorage.php index 2d6b7c6..2cff084 100644 --- a/includes/Data/RevisionStorage.php +++ b/includes/Data/RevisionStorage.php @@ -92,6 +92,96 @@ abstract protected function getRevType(); /** + * Insert revision state + * @param array + * @return boolean + */ + protected function insertRevState( array $rows ) { + // Revision state + $stateRows = array(); + foreach ( $rows as $i => $row ) { + $stateRows += array_values( unserialize( $this->splitUpdate( $row, 'frs' ) ) ); + } + if ( $stateRows ) { + $dbw = $this->dbFactory->getDB( DB_MASTER ); + $res = $dbw->insert( + 'flow_revision_state', + $this->preprocessSqlArray( $stateRows ), + __METHOD__ + ); + if ( !$res ) { + return false; + } + } + return true; + } + + /** + * Update revision state + * @param array + * @param array + * @return boolean + */ + protected function updateRevState( array $old, array $new ) { + $insert = $delete = array(); + $oldFrs = unserialize( $old['frs'] ); + $newFrs = unserialize( $new['frs'] ); + foreach ( $oldFrs as $state => $row ) { + if ( !isset( $newFrs[$state] ) ) { + $delete[] = $row; + } + } + foreach ( $newFrs as $state => $row ) { + if ( !isset( $oldFrs[$state] ) ) { + $insert[] = $row; + } + } + + foreach ( $delete as $row ) { + $res = $this->dbFactory->getDB( DB_MASTER )->delete( + 'flow_revision_state', + $this->preprocessSqlArray( array( 'frs_rev_id' => $row['frs_rev_id'], 'frs_rev_state' => $row['frs_rev_state'] ) ), + __METHOD__ + ); + if ( !$res ) { + return false; + } + } + + if ( $insert ) { + $res = $this->dbFactory->getDB( DB_MASTER )->insert( + 'flow_revision_state', + $this->preprocessSqlArray( $insert ), + __METHOD__ + ); + if ( !$res ) { + return false; + } + } + + return true; + } + + /** + * Delete revision state + * @param array + * @return boolean + */ + protected function removeRevState( array $row ) { + foreach ( unserialize( $row['frs'] ) as $stateRow ) { + $res = $this->dbFactory->getDB( DB_MASTER )->delete( + 'flow_revision_state', + $this->preprocessSqlArray( array( 'frs_rev_id' => $row['frs_rev_id'], 'frs_rev_state' => $row['frs_rev_state'] ) ), + __METHOD__ + ); + if ( !$res ) { + return false; + } + } + return true; + } + + /** * @param DbFactory $dbFactory * @param array|false List of externel store servers available for insert * or false to disable. See $wgFlowExternalStore. @@ -149,12 +239,63 @@ return $query; } + /** + * Find the state for revisions + */ + protected function findMultiRevState( array $revIds ) { + $result = array(); + + if ( !$revIds ) { + return $result; + } + + $dbr = $this->dbFactory->getDB( DB_MASTER ); + $res = $dbr->select( + array( 'flow_revision_state' ), + '*', + $this->preprocessSqlArray( array( 'frs_rev_id' => $revIds ) ), + __METHOD__ + ); + if ( !$res ) { + // TODO: dont fail, but dont end up caching bad result either + throw new DataModelException( 'query failure', 'process-data' ); + } + + foreach ( $res as $row ) { + $row = UUID::convertUUIDs( (array)$row, 'alphadecimal' ); + $result[$row['frs_rev_id']][$row['frs_rev_state']] = $row; + } + + return $result; + } + public function findMulti( array $queries, array $options = array() ) { if ( count( $queries ) < 3 ) { $res = $this->fallbackFindMulti( $queries, $options ); } else { $res = $this->findMultiInternal( $queries, $options ); } + $revIds = array(); + foreach ( $res as $revs ) { + foreach ( $revs as $rev ) { + $revIds[] = $rev['rev_id']; + } + } + $revState = $this->findMultiRevState( $revIds ); + foreach ( $res as $key => $revs ) { + foreach ( $revs as $revId => $data ) { + if ( isset( $revState[$data['rev_id']] ) ) { + $frs = $revState[$data['rev_id']]; + } else { + $frs = array(); + } + // Crap, need to serialize because FeatureIndex will throw + // an error if the data is of composite data type. Maybe just + // use a serialized field instead of a relational table + $res[$key][$revId]['frs'] = serialize( $frs ); + } + } + // Fetches content for all revisions flagged 'external' return self::mergeExternalContent( $res ); } @@ -377,6 +518,10 @@ return false; } + if ( !$this->insertRevState( $rows ) ) { + return false; + } + return $this->insertRelated( $rows ); } @@ -444,6 +589,11 @@ return false; } } + + if ( !$this->updateRevState( $old, $new ) ) { + return false; + } + return $this->updateRelated( $changeSet, $old ); } @@ -462,6 +612,10 @@ if ( !$res ) { return false; } + + if ( !$this->removeRevState( $row ) ) { + return false; + } return $this->removeRelated( $row ); } diff --git a/includes/Model/AbstractRevision.php b/includes/Model/AbstractRevision.php index 825f5da..810533c 100644 --- a/includes/Model/AbstractRevision.php +++ b/includes/Model/AbstractRevision.php @@ -28,7 +28,7 @@ self::MODERATED_HIDDEN, self::MODERATED_DELETED, self::MODERATED_SUPPRESSED, - self::MODERATED_CLOSED, + self::MODERATED_CLOSED ); /** @@ -155,11 +155,15 @@ */ protected $lastEditUserIp; - /** * @var string|null The wiki of the user that most recently changed the content */ protected $lastEditUserWiki; + + /** + * @var RevisionState[] + */ + protected $revisionState = array(); /** * @param string[] $row @@ -220,6 +224,13 @@ $obj->lastEditUserIp = isset( $row['rev_edit_user_ip'] ) ? $row['rev_edit_user_ip'] : null; $obj->lastEditUserWiki = isset( $row['rev_edit_user_wiki'] ) ? $row['rev_edit_user_wiki'] : null; + // Revision state + if ( isset( $row['frs'] ) ) { + foreach ( unserialize( $row['frs'] ) as $state ) { + $this->revisionState[$state->getState()] = RevisionState::fromStorageRow( $state ); + } + } + return $obj; } @@ -228,6 +239,18 @@ * @return string[] */ static public function toStorageRow( $obj ) { + $state = array(); + foreach ( $obj->revisionState as $state ) { + $state[$state->getState()] = serialize( array( + // Use getter method because PHP doesn't have package visibility + 'frs_rev_id' => $state->getRevId()->getAlphadecimal(), + 'frs_state' => $state->getState(), + 'frs_user_id' => $state->getUserId(), + 'frs_user_ip' => $state->getUserIp(), + 'frs_user_wiki' => $state->getUserWiki(), + 'frs_comment' => $state->getComment() + ) ); + } return array( 'rev_id' => $obj->revId->getAlphadecimal(), 'rev_user_id' => $obj->userId, @@ -253,6 +276,7 @@ 'rev_edit_user_id' => $obj->lastEditUserId, 'rev_edit_user_ip' => $obj->lastEditUserIp, 'rev_edit_user_wiki' => $obj->lastEditUserWiki, + 'frs' => $state ); } @@ -556,6 +580,13 @@ } /** + * @param string + */ + public function setChangeType( $changeType ) { + $this->changeType = $changeType; + } + + /** * @return string */ public function getModerationState() { @@ -563,6 +594,20 @@ } /** + * @return revisionState[] + */ + public function getRevisionState() { + return $this->revisionState; + } + + /** + * @param RevisionState[] + */ + public function setRevisionState( array $revisionState ) { + $this->revisionState = $revisionState; + } + + /** * @return string|null */ public function getModeratedReason() { diff --git a/includes/Model/RevisionState.php b/includes/Model/RevisionState.php new file mode 100644 index 0000000..ea8d05d --- /dev/null +++ b/includes/Model/RevisionState.php @@ -0,0 +1,137 @@ +<?php + +namespace Flow\Model; + +use Flow\Exception\DataModelException; +use User; + +/** + * Model class mapping to a revision state row + */ +class RevisionState { + + /** + * @var UUID revision id + */ + protected $revId; + + /** + * @var string Revision state + */ + protected $state; + + /** + * @var int|null User id setting the revision state + */ + protected $userId; + + /** + * @var string|null User ip setting the revision state + */ + protected $userIp; + + /** + * @var string User wiki setting the revision state + */ + protected $userWiki; + + /** + * @var string Comment for setting the revision state + */ + protected $comment; + + /** + * Create a RevisionState object + * + * @param User + * @param string + * @param string + * @return RevisionState + */ + public function create( User $user, $state, $comment = '' ) { + $obj = new self(); + $obj->revId = UUID::create(); + list( $obj->userId, $obj->userIp, $obj->userWiki ) = AbstractRevision::userFields( $user ); + $obj->comment = $comment; + return $obj; + } + + /** + * @param array + * @param RevisionState|null + * @return RevisionState + * @throws DataModelException + */ + public static function fromStorageRow( array $row, $obj = null ) { + if ( $obj === null ) { + $obj = new self; + } elseif ( !$obj instanceof self ) { + throw new DataModelException( 'Wrong obj type: ' . get_class( $obj ), 'process-data' ); + } + $obj->revId = UUID::create( $row['frs_rev_id'] ); + $obj->state = $row['frs_state']; + $obj->userId = $row['frs_user_id']; + $obj->userIp = $row['frs_user_ip']; + $obj->userWiki = $row['frs_user_wiki']; + $obj->comment = $row['frs_comment']; + return $obj; + } + + /** + * @param RevisionState + * @return array + */ + public static function toStorageRow( RevisionState $obj ) { + return array( + 'frs_rev_id' => $obj->revId->getBinary(), + 'frs_state' => $obj->state, + 'frs_user_id' => $obj->userId, + 'frs_user_ip' => $obj->userIp, + 'frs_user_wiki' => $obj->userWiki, + 'frs_comment' => $obj->comment, + ); + } + + /** + * @return UUID + */ + public function getRevId() { + return $this->revId; + } + + /** + * @return string + */ + public function getState() { + return $this->state; + } + + /** + * @return int + */ + public function getUserId() { + return $this->userId; + } + + /** + * @return string + */ + public function getUserIp() { + return $this->userIp; + } + + /** + * @return string + */ + public function getUserWiki() { + return $this->userWiki; + } + + /** + * @return string + */ + public function getComment() { + return $this->comment; + } + +} -- To view, visit https://gerrit.wikimedia.org/r/134982 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I269b5cd143ea956b670d46483486b3cecaeebcd7 Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/extensions/Flow Gerrit-Branch: master Gerrit-Owner: Bsitu <bs...@wikimedia.org> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits