jenkins-bot has submitted this change and it was merged.

Change subject: Draft translations - save translations and resume from dashboard
......................................................................


Draft translations - save translations and resume from dashboard

* Translations can be saved as draft before publishing.
* Saved translation can be restored from the dashboard.

* Modified ApiQueryContentTranslation to get a specific
  translation with draft content if translation id is passed.
  But I see a possibility of an API dedicated to drafts when
  we start supporting deleting.

Saving translations:
* Instead of publish button a save button will be shown. Once
  saved, the button changes to Publish
* Once published if translator changes anything, Save button activates
  again
* Control+S shortcut is remapped to Save feature from Publish feature
* Non-empty sections from translation column alone saved as draft content.
* The HTML DOM will reflect all translation workflow state trackers
  such as progress information, MT/Human/Source annotations
* Translation id is reused as draft id as well.
* There is only one version of draft at any time - latest version
* Translation drafts are not connected to translators. Same is the case
  of translation. For a given source-target language pair and source
  page, there exists only one translation. Only one draft exists.
  This means, for that pair somebody else can overwrite others drafts.
  This need to be addressed later.

Resuming translation:
* In translation view, if draft id is passed as URL parameter, the draft
  content will be loaded and aligned using a best try approach: by matching
  data-source attributes. If no match found, skip.
* Translation dashboard's translation listing provides a link to resume 
translations

The draft feature will be disabled, if the CX common database is not configured.
The save button will not be presented in that case. There is no translation
listing either when common database not configured.

Change-Id: If97c80f6df69dab9eac68faf7d9a4fc0097df837
---
M Autoload.php
M ContentTranslation.hooks.php
M Resources.php
M api/ApiContentTranslationPublish.php
M api/ApiQueryContentTranslation.php
M hooks.md
M i18n/en.json
M i18n/qqq.json
A includes/Draft.php
M includes/Translation.php
M modules/dashboard/ext.cx.translationlist.js
A modules/draft/ext.cx.draft.js
M modules/editor/ext.cx.editor.js
M modules/header/ext.cx.header.js
M modules/header/styles/ext.cx.header.less
M modules/publish/ext.cx.publish.js
M modules/tools/ext.cx.tools.mt.js
M modules/translation/ext.cx.translation.js
M modules/translationview/ext.cx.translationview.js
M sql/contenttranslation.sql
20 files changed, 391 insertions(+), 74 deletions(-)

Approvals:
  Nikerabbit: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/Autoload.php b/Autoload.php
index e853bdb..b2c8bd6 100644
--- a/Autoload.php
+++ b/Autoload.php
@@ -15,6 +15,7 @@
        'ApiQueryContentTranslation' => 
"$dir/api/ApiQueryContentTranslation.php",
        'ContentTranslationHooks' => "$dir/ContentTranslation.hooks.php",
        'ContentTranslation\Database' => "$dir/includes/Database.php",
+       'ContentTranslation\Draft' => "$dir/includes/Draft.php",
        'ContentTranslation\GlobalUser' => "$dir/includes/GlobalUser.php",
        'ContentTranslation\SiteMapper' => "$dir/includes/SiteMapper.php",
        'ContentTranslation\Stats' => "$dir/includes/Stats.php",
diff --git a/ContentTranslation.hooks.php b/ContentTranslation.hooks.php
index 5be3f6a..513b5ca 100644
--- a/ContentTranslation.hooks.php
+++ b/ContentTranslation.hooks.php
@@ -76,6 +76,7 @@
                global $wgContentTranslationServerURL,
                        $wgContentTranslationTranslateInTarget,
                        $wgContentTranslationExperimentalFeatures,
+                       $wgContentTranslationDatabase,
                        $wgContentTranslationSiteTemplates;
 
                // Temporary BC code for old configuration
@@ -86,5 +87,6 @@
                $vars['wgContentTranslationSiteTemplates'] = 
$wgContentTranslationSiteTemplates;
                $vars['wgContentTranslationTranslateInTarget'] = 
$wgContentTranslationTranslateInTarget;
                $vars['wgContentTranslationExperimentalFeatures'] = 
$wgContentTranslationExperimentalFeatures;
+               $vars['wgContentTranslationDatabase'] = 
$wgContentTranslationDatabase;
        }
 }
diff --git a/Resources.php b/Resources.php
index cf831e4..b0562fa 100644
--- a/Resources.php
+++ b/Resources.php
@@ -100,6 +100,7 @@
                'cx-error-page-not-found',
                'cx-header-new-translation',
                'cx-publish-button',
+               'cx-save-draft-button',
                'cx-special-login-error',
                'cx-translation-target-page-exists',
                'login',
@@ -408,6 +409,19 @@
        ),
 ) + $resourcePaths;
 
+$wgResourceModules['ext.cx.draft'] = array(
+       'scripts' => array(
+               'draft/ext.cx.draft.js',
+       ),
+       'dependencies' => array(
+               'ext.cx.model',
+               'mediawiki.api.edit',
+       ),
+       'messages' => array(
+               'cx-save-draft-saving',
+       ),
+) + $resourcePaths;
+
 $wgResourceModules['ext.cx.publish'] = array(
        'scripts' => array(
                'publish/ext.cx.publish.js',
diff --git a/api/ApiContentTranslationPublish.php 
b/api/ApiContentTranslationPublish.php
index d43a990..fde9d34 100644
--- a/api/ApiContentTranslationPublish.php
+++ b/api/ApiContentTranslationPublish.php
@@ -96,11 +96,21 @@
        public function execute() {
                $params = $this->extractRequestParams();
 
+               if ( $params['status'] === 'draft' ) {
+                       $this->saveAsDraft();
+               } else {
+                       $this->publish();
+               }
+
+       }
+
+       public function publish() {
+               $params = $this->extractRequestParams();
+
                $title = Title::newFromText( $params['title'] );
                if ( !$title ) {
                        $this->dieUsageMsg( 'invalidtitle', $params['title'] );
                }
-
                try {
                        $wikitext = $this->convertHtmlToWikitext( $title, 
$params['html'] );
                } catch ( MWException $e ) {
@@ -142,6 +152,15 @@
                $this->getResult()->addValue( null, $this->getModuleName(), 
$result );
        }
 
+       public function saveAsDraft() {
+               $params = $this->extractRequestParams();
+               $this->saveTranslationHistory( $params );
+               $result = array(
+                       'result' => 'success',
+               );
+               $this->getResult()->addValue( null, $this->getModuleName(), 
$result );
+       }
+
        public function saveTranslationHistory( $params ) {
                global $wgContentTranslationDatabase;
 
@@ -162,14 +181,18 @@
                        'targetURL' => 
ContentTranslation\SiteMapper::getPageURL(
                                $params['to'], $params['title']
                        ),
-                       'status' => 'published',
+                       'status' => $params['status'],
                        'progress' => $params['progress'],
                        // XXX Do not overwrite startedTranslator when we have 
"draft save" feature.
                        'startedTranslator' => $translator->getGlobalUserId(),
                        'lastUpdatedTranslator' => 
$translator->getGlobalUserId(),
                ) );
                $translation->save();
-               $translator->addTranslation( $translation->getTranslationId() );
+               $translationId = $translation->getTranslationId();
+               $translator->addTranslation(  $translationId );
+               if ( $params['status'] === 'draft' ) {
+                       ContentTranslation\Draft::save( $translationId, 
$params['html'] );
+               }
        }
 
        public function getAllowedParams() {
@@ -197,6 +220,10 @@
                        ),
                        'sourcerevision' => array(
                                ApiBase::PARAM_REQUIRED => true,
+                       ),
+                       'status' => array(
+                               ApiBase::PARAM_REQUIRED => true,
+                               ApiBase::PARAM_TYPE => array( 'draft', 
'published' ),
                        ),
                        'categories' => null,
                        /** @todo These should be renamed to something 
all-lowercase and lacking a "wp" prefix */
@@ -233,6 +260,8 @@
                        'to' => 'The target language code.',
                        'sourcetitle' => 'The title of the source page.',
                        'sourcerevision' => 'The revision of the source page.',
+                       'status' => 'If draft, translation will be saved as 
draft.
+                               If published, translation will be published',
                        'progress' => 'Translation progress as JSON.',
                        'categories' => 'The categories to be added to the 
published page.',
                        'wpCaptchaId' => 'Captcha ID (when saving with a 
captcha response).',
diff --git a/api/ApiQueryContentTranslation.php 
b/api/ApiQueryContentTranslation.php
index a4946e2..e109e82 100644
--- a/api/ApiQueryContentTranslation.php
+++ b/api/ApiQueryContentTranslation.php
@@ -32,31 +32,41 @@
         */
        private function run( $resultPageSet = null ) {
                $params = $this->extractRequestParams();
-               $translator = new ContentTranslation\Translator( 
User::newFromName( $params['user'] ) );
-               $translations = $translator->getAllTranslations();
                $result = $this->getResult();
-               $result->addValue(
-                       array( 'query', 'contenttranslation' ),
-                       'translator',
-                       $params['user']
-               );
-               $result->addValue(
-                       array( 'query', 'contenttranslation' ),
-                       'translations',
-                       $translations
-               );
-               $result->addValue(
-                       array( 'query', 'contenttranslation' ),
-                       'resultsize',
-                       count( $translations )
-               );
+
+               if ( $params['translationid'] ) {
+                       $translation = 
ContentTranslation\Translation::newFromId( $params['translationid'] );
+                       $result->addValue(
+                               array( 'query', 'contenttranslation' ),
+                               'translations',
+                               $translation
+                       );
+               } else {
+                       if ( !isset( $params['user'] ) ) {
+                               $this->dieUsageMsg( array( 'missingparam', 
'user' ) );
+                       }
+                       $translator = new ContentTranslation\Translator( 
User::newFromName( $params['user'] ) );
+                       $translations = $translator->getAllTranslations();
+                       $result->addValue(
+                               array( 'query', 'contenttranslation' ),
+                               'translations',
+                               $translations
+                       );
+                       $result->addValue(
+                               array( 'query', 'contenttranslation' ),
+                               'translator',
+                               $params['user']
+                       );
+               }
        }
 
        public function getAllowedParams() {
                $allowedParams = array(
                        'user' => array(
                                ApiBase::PARAM_TYPE => 'string',
-                               ApiBase::PARAM_REQUIRED => true,
+                       ),
+                       'translationid' => array(
+                               ApiBase::PARAM_TYPE => 'string',
                        ),
                        'limit' => array(
                                ApiBase::PARAM_DFLT => 10,
@@ -72,6 +82,7 @@
        public function getParamDescription() {
                $paramDescs = array(
                        'user' => 'Username of the translator.',
+                       'translationid' => 'Translation id',
                );
 
                return $paramDescs;
@@ -84,6 +95,7 @@
        protected function getExamples() {
                return array(
                        
'api.php?action=query&list=contenttranslation&user=Santhosh',
+                       
'api.php?action=query&list=contenttranslation&translationid=94',
                );
        }
 }
diff --git a/hooks.md b/hooks.md
index 86f8ef6..ad694fb 100644
--- a/hooks.md
+++ b/hooks.md
@@ -109,3 +109,7 @@
 ## mw.cx.translation.updated
 
 Fired when the translation section was updated using the MT card, for example 
with the 'restore' or 'use source text' actions.
+
+##  mw.cx.translation.placeholders.ready
+
+Fired after every section in the translation column has been filled with a 
placeholder.
\ No newline at end of file
diff --git a/i18n/en.json b/i18n/en.json
index 6508032..b35c5e2 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -104,7 +104,10 @@
        "apihelp-cxpublish-param-to": "The target language code.",
        "apihelp-cxpublish-param-sourcetitle": "The title of the source page.",
        "apihelp-cxpublish-param-sourcerevision": "The revision of the source 
page.",
+       "apihelp-cxpublish-param-status": "If draft, translation will be saved 
as draft. If published, translation will be published.",
        "apihelp-cxpublish-param-categories": "The categories to be added to 
the published page.",
        "apihelp-cxpublish-param-wpCaptchaId": "Captcha ID (when saving with a 
captcha response).",
-       "apihelp-cxpublish-param-wpCaptchaWord": "Answer to the captcha (when 
saving with a captcha response)."
+       "apihelp-cxpublish-param-wpCaptchaWord": "Answer to the captcha (when 
saving with a captcha response).",
+       "cx-save-draft-button": "Save as draft",
+       "cx-save-draft-saving": "Saving..."
 }
diff --git a/i18n/qqq.json b/i18n/qqq.json
index fa1acd8..b838639 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -108,7 +108,10 @@
        "apihelp-cxpublish-param-to": "{{doc-apihelp-param|cxpublish|to}}",
        "apihelp-cxpublish-param-sourcetitle": 
"{{doc-apihelp-param|cxpublish|sourcetitle}}",
        "apihelp-cxpublish-param-sourcerevision": 
"{{doc-apihelp-param|cxpublish|sourcerevision}}",
+       "apihelp-cxpublish-param-status": 
"{{doc-apihelp-param|cxpublish|status}}",
        "apihelp-cxpublish-param-categories": 
"{{doc-apihelp-param|cxpublish|categories}}",
        "apihelp-cxpublish-param-wpCaptchaId": 
"{{doc-apihelp-param|cxpublish|wpCaptchaId}}",
-       "apihelp-cxpublish-param-wpCaptchaWord": 
"{{doc-apihelp-param|cxpublish|wpCaptchaWord}}"
+       "apihelp-cxpublish-param-wpCaptchaWord": 
"{{doc-apihelp-param|cxpublish|wpCaptchaWord}}",
+       "cx-save-draft-button": "Label of button to save the translation as 
draft.",
+       "cx-save-draft-saving": "Label of button to save the translation as 
draft while saving is in progress"
 }
diff --git a/includes/Draft.php b/includes/Draft.php
new file mode 100644
index 0000000..e7f5569
--- /dev/null
+++ b/includes/Draft.php
@@ -0,0 +1,25 @@
+<?php
+/**
+ * ContentTranslation Translation Draft
+ * Database access method for drafts table.
+ */
+namespace ContentTranslation;
+
+class Draft {
+       public static function save( $translationId, $content ) {
+               $dbw = Database::getConnection( DB_MASTER );
+               $values = array(
+                       'draft_id' => $translationId,
+                       'draft_content' => $content,
+                       'draft_timestamp' => $dbw->timestamp(),
+               );
+               $dbw->upsert(
+                       'drafts',
+                       $values,
+                       array( 'draft_id' ),
+                       $values,
+                       __METHOD__
+               );
+       }
+
+}
diff --git a/includes/Translation.php b/includes/Translation.php
index 7f04d76..954956b 100644
--- a/includes/Translation.php
+++ b/includes/Translation.php
@@ -10,25 +10,27 @@
        }
 
        public function save() {
-               $dbw = Database::getConnection( DB_SLAVE );
-               $dbw->replace(
+               $dbw = Database::getConnection( DB_MASTER );
+               $values = array(
+                       'translation_source_title' => 
$this->translation['sourceTitle'],
+                       'translation_target_title' => 
$this->translation['targetTitle'],
+                       'translation_source_language' => 
$this->translation['sourceLanguage'],
+                       'translation_target_language' => 
$this->translation['targetLanguage'],
+                       'translation_source_url' => 
$this->translation['sourceURL'],
+                       'translation_target_url' => 
$this->translation['targetURL'],
+                       'translation_status' => $this->translation['status'],
+                       // XXX do not overwrite when we have "draft save" 
feature.
+                       'translation_start_timestamp' => $dbw->timestamp(),
+                       'translation_last_updated_timestamp' => 
$dbw->timestamp(),
+                       'translation_progress' => 
$this->translation['progress'],
+                       'translation_started_by' => 
$this->translation['startedTranslator'],
+                       'translation_last_update_by' => 
$this->translation['lastUpdatedTranslator'],
+               );
+               $dbw->upsert(
                        'translations',
+                       $values,
                        array( 'translation_id' ),
-                       array(
-                               'translation_source_title' => 
$this->translation['sourceTitle'],
-                               'translation_target_title' => 
$this->translation['targetTitle'],
-                               'translation_source_language' => 
$this->translation['sourceLanguage'],
-                               'translation_target_language' => 
$this->translation['targetLanguage'],
-                               'translation_source_url' => 
$this->translation['sourceURL'],
-                               'translation_target_url' => 
$this->translation['targetURL'],
-                               'translation_status' => 
$this->translation['status'],
-                               // XXX do not overwrite when we have "draft 
save" feature.
-                               'translation_start_timestamp' => 
$dbw->timestamp(),
-                               'translation_last_updated_timestamp' => 
$dbw->timestamp(),
-                               'translation_progress' => 
$this->translation['progress'],
-                               'translation_started_by' => 
$this->translation['startedTranslator'],
-                               'translation_last_update_by' => 
$this->translation['lastUpdatedTranslator'],
-                       ),
+                       $values,
                        __METHOD__
                );
 
@@ -39,6 +41,24 @@
 
        public function getTranslationId() {
                return $this->translation['id'];
+       }
+
+       public static function newFromId( $translationId ) {
+               $dbr = Database::getConnection( DB_SLAVE );
+               $rows = $dbr->select(
+                       array( 'translations', 'drafts' ),
+                       '*',
+                       array(
+                               'translation_id' => $translationId,
+                               'draft_id' => $translationId,
+                       ),
+                       __METHOD__
+               );
+               $result = array();
+               foreach ( $rows as $row ) {
+                       $result[] = Translation::newFromRow( $row );
+               }
+               return $result;
        }
 
        /**
@@ -59,6 +79,8 @@
                        'progress' => $row->translation_progress,
                        'startedTranslator' => $row->translation_started_by,
                        'lastUpdatedTranslator' => 
$row->translation_last_update_by,
+                       'draftContent' =>  isset( $row->draft_content ) ? 
$row->draft_content: null,
+                       'draftTimestamp' =>  isset( $row->draft_timestamp ) ? 
$row->draft_timestamp: null,
                ) );
 
                return $translation;
diff --git a/modules/dashboard/ext.cx.translationlist.js 
b/modules/dashboard/ext.cx.translationlist.js
index 0e5fb79..7da8183 100644
--- a/modules/dashboard/ext.cx.translationlist.js
+++ b/modules/dashboard/ext.cx.translationlist.js
@@ -94,7 +94,7 @@
         * @param {Object[]} translations
         */
        CXTranslationList.prototype.listTranslations = function ( translations 
) {
-               var i, translation, $translation, $titleLanguageBlock, 
$sourceLink, $sourceLanguage,
+               var i, translation, $translation, $titleLanguageBlock, 
$translationLink, $sourceLanguage,
                        $targetLanguage, $imageBlock, $lastUpdated, $image, 
$status, $progressbar;
 
                for ( i = 0; i < translations.length; i++ ) {
@@ -118,11 +118,15 @@
                        $imageBlock.append( $image, $progressbar );
                        this.showTitleImage( translation );
 
-                       $sourceLink = $( '<a>' )
+                       $translationLink = $( '<a>' )
                                .addClass( 'source-title' )
                                .attr( {
-                                       href: translation.sourceURL,
-                                       target: '_blank'
+                                       href: new mw.Uri().extend( {
+                                               from: 
translation.sourceLanguage,
+                                               to: translation.targetLanguage,
+                                               page: translation.sourceTitle,
+                                               draft: translation.status === 
'draft' ? translation.id : undefined
+                                       } ).toString(),
                                } ).text( translation.sourceTitle );
                        $sourceLanguage = $( '<div>' )
                                .addClass( 'source-language' )
@@ -135,7 +139,7 @@
                                .text( translation.status );
                        $titleLanguageBlock = $( '<div>' )
                                .addClass( 'title-language-block' )
-                               .append( $sourceLink, $sourceLanguage, 
$targetLanguage, $status );
+                               .append( $translationLink, $sourceLanguage, 
$targetLanguage, $status );
                        $translation.append(
                                $lastUpdated,
                                $imageBlock,
diff --git a/modules/draft/ext.cx.draft.js b/modules/draft/ext.cx.draft.js
new file mode 100644
index 0000000..5adad15
--- /dev/null
+++ b/modules/draft/ext.cx.draft.js
@@ -0,0 +1,158 @@
+/**
+ * ContentTranslation - Save translation as draft
+ *
+ * @file
+ * @ingroup Extensions
+ * @copyright See AUTHORS.txt
+ * @license GPL-2.0+
+ */
+( function ( $, mw ) {
+       'use strict';
+
+       /**
+        * @class
+        * @param {number} draftId Draft id
+        */
+       function ContentTranslationDraft( draftId ) {
+               this.draftId = draftId;
+               this.$draftButton = $( '.cx-header__draft-button' );
+               this.listen();
+       }
+
+       /**
+        * Event bindings
+        */
+       ContentTranslationDraft.prototype.listen = function () {
+               var self = this;
+               mw.hook( 'mw.cx.save' ).add( function () {
+                       self.save();
+               } );
+               mw.hook( 'mw.cx.translation.published' ).add( function () {
+                       self.$draftButton.prop( 'disabled', true ).show();
+               } );
+               // Publish when CTRL+S is pressed.
+               $( document ).on( 'keydown', function ( e ) {
+                       if ( e.ctrlKey && e.which === 83 ) {
+                               e.preventDefault();
+                               mw.hook( 'mw.cx.save' ).fire();
+                               return false;
+                       }
+               } );
+       };
+
+       /**
+        * Fetch a draft content and restore it.
+        */
+       ContentTranslationDraft.prototype.fetch = function () {
+               var self = this,
+                       api = new mw.Api();
+
+               // TODO: The fetch can start immediately when the module loaded
+               // Only the restoring part need to delay till placeholders are 
rendered.
+               // Now there is a visible delay between placeholder rendering 
and restoring draft.
+               api.get( {
+                       action: 'query',
+                       list: 'contenttranslation',
+                       translationid: this.draftId,
+                       format: 'json'
+               } ).done( function ( response ) {
+                       var translations, translation, draftContent;
+                       translations = 
response.query.contenttranslation.translations;
+                       translation = translations[ 0 ].translation;
+                       draftContent = translation.draftContent;
+                       self.$draft = $( draftContent );
+                       self.restore();
+               } );
+       };
+
+       /**
+        * Restore this draft to the appropriate placeholders
+        */
+       ContentTranslationDraft.prototype.restore = function () {
+               var i, $draftSection, sectionId, $section;
+
+               for ( i = 0; i < this.$draft.length; i++ ) {
+                       $draftSection = $( this.$draft[ i ] );
+                       sectionId = $draftSection.prop( 'id' );
+                       $section = $( '#' + sectionId );
+                       $section.replaceWith( $draftSection );
+                       // Get new section
+                       $section = $( '#' + sectionId );
+                       mw.hook( 'mw.cx.translation.postMT' ).fire( $section );
+               }
+       };
+
+       /**
+        * Save the translation
+        */
+       ContentTranslationDraft.prototype.save = function () {
+               var self = this,
+                       translatedTitle,
+                       draftContent, targetTitle, params, apiParams,
+                       api = new mw.Api();
+
+               translatedTitle = $( '.cx-column--translation > h2' ).text();
+               draftContent = $( '.cx-column--translation .cx-column__content' 
).clone();
+               targetTitle = 'User:' + mw.user.getName() + '/' + 
translatedTitle;
+
+               this.$draftButton
+                       .prop( 'disabled', true )
+                       .text( mw.msg( 'cx-save-draft-saving' ) );
+               params = {
+                       from: mw.cx.sourceLanguage,
+                       to: mw.cx.targetLanguage,
+                       sourcetitle: mw.cx.sourceTitle,
+                       title: targetTitle,
+                       html: prepareTranslationForSave( draftContent ),
+                       status: 'draft',
+                       sourcerevision: mw.cx.sourceRevision,
+                       progress: JSON.stringify( mw.cx.getProgress() )
+               };
+
+               apiParams = $.extend( {}, params, {
+                       action: 'cxpublish'
+               } );
+               api.postWithToken( 'edit', apiParams, {
+                       timeout: 100 * 1000 // in milliseconds
+               } ).done( function () {
+                       mw.hook( 'mw.cx.translation.saved' ).fire();
+               } ).fail( function () {
+                       mw.hook( 'mw.cx.error' ).fire( mw.msg( 
'cx-publish-page-error' ) );
+               } ).always( function () {
+                       self.$draftButton
+                               .prop( 'disabled', false )
+                               .text( mw.msg( 'cx-save-draft-button' ) )
+                               .hide();
+               } );
+       };
+
+       /**
+        * Prepare the translated content for publishing by removing
+        * unwanted parts.
+        * @return {string} processed html
+        */
+       function prepareTranslationForSave( $content ) {
+               // Remove empty sections.
+               $content
+                       .filter( function () {
+                               return !$.trim( $( this ).text() ) && $( this 
).children().length;
+                       } )
+                       .remove();
+               // Remove placeholder sections
+               $content.find( '.placeholder' ).remove();
+               return $content.html();
+       }
+
+
+       $( function () {
+               var drafId, draft;
+
+               drafId = new mw.Uri().query.draft;
+               draft = new ContentTranslationDraft( drafId );
+               if ( drafId ) {
+                       mw.hook( 'mw.cx.translation.placeholders.ready' ).add( 
function () {
+                               draft.fetch();
+                       } );
+               }
+       } );
+}( jQuery, mediaWiki ) );
diff --git a/modules/editor/ext.cx.editor.js b/modules/editor/ext.cx.editor.js
index 9fd4186..0018cf3 100644
--- a/modules/editor/ext.cx.editor.js
+++ b/modules/editor/ext.cx.editor.js
@@ -42,7 +42,10 @@
        CXSectionEditor.prototype.onChange = function () {
                // Remove the MT/source/empty label from the section.
                // Some manual change happened.
-               this.$section.data( 'cx-state', null );
+               // Need to do both to actually null the data - so that it 
reflects in
+               // jquery cache and DOM http://api.jquery.com/removedata/
+               this.$section.removeData( 'cx-state' );
+               this.$section.removeAttr( 'data-cx-state' );
                mw.hook( 'mw.cx.translation.change' ).fire( this.$section );
        };
 
diff --git a/modules/header/ext.cx.header.js b/modules/header/ext.cx.header.js
index 5a7984f..d22576f 100644
--- a/modules/header/ext.cx.header.js
+++ b/modules/header/ext.cx.header.js
@@ -23,6 +23,7 @@
                this.siteMapper = siteMapper;
 
                this.$publishButton = null;
+               this.$draftButton = null;
                this.$infoBar = null;
 
                this.init();
@@ -38,7 +39,14 @@
         * @param {object} weights
         */
        ContentTranslationHeader.prototype.setPublishButtonState = function ( 
weights ) {
-               this.$publishButton.show().prop( 'disabled', weights.any === 0 
);
+               if ( mw.config.get( 'wgContentTranslationDatabase' ) !== null ) 
{
+                       this.$draftButton.show().prop( 'disabled', weights.any 
=== 0 );
+                       this.$publishButton.hide();
+               } else {
+                       this.$publishButton.show().prop( 'disabled', 
weights.any === 0 );
+               }
+
+
        };
 
        /**
@@ -132,6 +140,9 @@
                this.$publishButton.on( 'click', function () {
                        mw.hook( 'mw.cx.publish' ).fire();
                } );
+               this.$draftButton.on( 'click', function () {
+                       mw.hook( 'mw.cx.save' ).fire();
+               } );
 
                // Click handler for remove icon in info bar.
                this.$infoBar.on( 'click', '.remove', function () {
@@ -164,8 +175,8 @@
                        .append( $logo, $titleText );
 
                $translationCenterLink = $( '<a>' )
-                       // TODO update the text when the dashboard is ready
-                       .text( mw.msg( 'cx-header-new-translation' ) )
+               // TODO update the text when the dashboard is ready
+               .text( mw.msg( 'cx-header-new-translation' ) )
                        .attr( 'href', mw.util.getUrl( 
'Special:ContentTranslation' ) );
 
                $translationCenter = $( '<div>' )
@@ -178,9 +189,20 @@
                        .text( mw.msg( 'cx-publish-button' ) )
                        .hide();
 
+               if ( mw.config.get( 'wgContentTranslationDatabase' ) !== null ) 
{
+                       this.$draftButton = $( '<button>' )
+                               .addClass( 'cx-header__draft-button 
mw-ui-button mw-ui-constructive' )
+                               .prop( 'disabled', true )
+                               .text( mw.msg( 'cx-save-draft-button' ) )
+                               .hide();
+                       this.$publishButton.prop( 'disabled', false );
+               } else {
+                       this.$draftButton = $();
+               }
+
                $publishArea = $( '<div>' )
                        .addClass( 'cx-header__publish' )
-                       .append( this.$publishButton );
+                       .append( this.$draftButton, this.$publishButton );
 
                $headerBar = $( '<div>' )
                        .addClass( 'cx-header__bar' )
diff --git a/modules/header/styles/ext.cx.header.less 
b/modules/header/styles/ext.cx.header.less
index 0fc3665..5ac42d1 100644
--- a/modules/header/styles/ext.cx.header.less
+++ b/modules/header/styles/ext.cx.header.less
@@ -113,7 +113,8 @@
                margin: 0;
        }
 
-       .cx-header__publish-button {
+       .cx-header__publish-button,
+       .cx-header__draft-button {
                float: right;
        }
 
diff --git a/modules/publish/ext.cx.publish.js 
b/modules/publish/ext.cx.publish.js
index 3f23614..79bc312 100644
--- a/modules/publish/ext.cx.publish.js
+++ b/modules/publish/ext.cx.publish.js
@@ -107,12 +107,12 @@
         */
        function publish() {
                var $publishArea, $publishButton, publisher, translatedTitle,
-                       translatedContent, targetTitle, targetCategories,
+                       translatedContent, targetTitle, targetCategories, 
$draftButton,
                        sortedKeys, i, categoryTitles, categories;
 
                $publishArea = $( '.cx-header__publish' );
                $publishButton = $publishArea.find( 
'.cx-header__publish-button' );
-
+               $draftButton = $publishArea.find( '.cx-header__draft-button' );
                translatedTitle = $( '.cx-column--translation > h2' ).text();
                translatedContent = prepareTranslationForPublish(
                        $( '.cx-column--translation .cx-column__content' 
).clone()
@@ -139,6 +139,7 @@
                        sourcetitle: mw.cx.sourceTitle,
                        title: targetTitle,
                        html: translatedContent,
+                       status: 'published',
                        sourcerevision: mw.cx.sourceRevision,
                        categories: categories,
                        progress: JSON.stringify( mw.cx.getProgress() )
@@ -167,7 +168,8 @@
                } ).always( function () {
                        $publishButton
                                .prop( 'disabled', false )
-                               .text( mw.msg( 'cx-publish-button' ) );
+                               .text( mw.msg( 'cx-publish-button' ) )
+                               .hide();
                } );
 
                initGuidedTour( translatedTitle );
@@ -216,13 +218,8 @@
 
        $( function () {
                mw.hook( 'mw.cx.publish' ).add( $.proxy( publish, this ) );
-               // Publish when CTRL+S is pressed.
-               $( document ).on( 'keydown', function ( e ) {
-                       if ( e.ctrlKey && e.which === 83 ) {
-                               e.preventDefault();
-                               mw.hook( 'mw.cx.publish' ).fire();
-                               return false;
-                       }
+               mw.hook( 'mw.cx.translation.saved' ).add( function () {
+                       $( '.cx-header__publish-button' ).show();
                } );
        } );
 }( jQuery, mediaWiki ) );
diff --git a/modules/tools/ext.cx.tools.mt.js b/modules/tools/ext.cx.tools.mt.js
index 41e9ba0..b65ff61 100644
--- a/modules/tools/ext.cx.tools.mt.js
+++ b/modules/tools/ext.cx.tools.mt.js
@@ -177,9 +177,9 @@
                                                .children()
                                                .attr( {
                                                        id: 'cx' + sourceId,
-                                                       'data-source': sourceId
+                                                       'data-source': sourceId,
+                                                       'data-cx-state': 'mt'
                                                } )
-                                               .data( 'cx-state', 'mt' )
                                        );
                                        // $section was replaced. Get the 
updated instance.
                                        $section = $( '#cx' + sourceId );
diff --git a/modules/translation/ext.cx.translation.js 
b/modules/translation/ext.cx.translation.js
index 519141a..4c1700b 100644
--- a/modules/translation/ext.cx.translation.js
+++ b/modules/translation/ext.cx.translation.js
@@ -127,6 +127,7 @@
                        // there is a chance for section misalignment
                        window.setTimeout( function () {
                                cxTranslation.addPlaceholders();
+                               mw.hook( 'mw.cx.translation.placeholders.ready' 
).fire();
                        }, 2000 );
                } );
 
@@ -229,12 +230,12 @@
                                .clone()
                                .attr( {
                                        id: 'cx' + sourceId,
-                                       'data-source': sourceId
-                               } )
-                               .data( 'cx-state', 'source' );
+                                       'data-source': sourceId,
+                                       'data-cx-state': 'source',
+                               } );
 
                        if ( origin === 'mt-user-disabled' || origin === 
'clear' ) {
-                               $clone.data( 'cx-state', 'empty' );
+                               $clone.attr( 'data-cx-state', 'empty' );
                                $clone.empty();
                        } // else: service-failure, non-editable, 
mt-not-available
 
@@ -347,7 +348,7 @@
                        $sourceSection = $( $sourceSections[ i ] );
                        sourceSectionId = $sourceSection.attr( 'id' );
                        $placeholder = getPlaceholder( sourceSectionId )
-                               .data( 'cx-section-type', $sourceSection.prop( 
'tagName' ) );
+                               .attr( 'data-cx-section-type', 
$sourceSection.prop( 'tagName' ) );
                        placeholders.push( $placeholder );
 
                        // Bind events to the placeholder sections
diff --git a/modules/translationview/ext.cx.translationview.js 
b/modules/translationview/ext.cx.translationview.js
index d76d6d3..03f0a64 100644
--- a/modules/translationview/ext.cx.translationview.js
+++ b/modules/translationview/ext.cx.translationview.js
@@ -49,7 +49,8 @@
                                'ext.cx.tools',
                                'ext.cx.translation',
                                'ext.cx.translation.progress',
-                               'ext.cx.publish'
+                               'ext.cx.publish',
+                               'ext.cx.draft'
                        ] ).then( function () {
                                cx.$translation.cxTranslation();
                                cx.$tools.cxTools();
diff --git a/sql/contenttranslation.sql b/sql/contenttranslation.sql
index 3fe8ed2..368d0e0 100644
--- a/sql/contenttranslation.sql
+++ b/sql/contenttranslation.sql
@@ -6,17 +6,18 @@
 -- mysql> GRANT ALL ON contenttranslation.* to 'USER'@'localhost';
 -- mysql> SOURCE contenttranslation.sql
 
+DROP TABLE IF EXISTS /*_*/translations;
 CREATE TABLE /*_*/translations (
       -- translation id. Autogenerated.
       translation_id int primary key auto_increment,
       -- Source title of the translation
-      translation_source_title varchar(512) binary not null,
+      translation_source_title varbinary(512) not null,
       -- Target title of the translation
-      translation_target_title varchar(512) binary not null,
+      translation_target_title varbinary(512) not null,
       -- Source language. language code
-      translation_source_language varchar(36) binary not null,
+      translation_source_language varbinary(36) not null,
       -- Target language. language code
-      translation_target_language varchar(36) binary not null,
+      translation_target_language varbinary(36) not null,
       -- source of the page as full canonical url -- 
https://www.mediawiki.org/wiki/Help:CxIsPage
       translation_source_url text binary not null,
       -- link to the draft/published target
@@ -36,11 +37,25 @@
       translation_last_update_by int
 ) /*$wgDBTableOptions*/;
 
+DROP TABLE IF EXISTS /*_*/translators;
 CREATE TABLE /*_*/translators (
-      -- Translators id - global user id?
+      -- Translators id - global user id
       translator_user_id int not null,
       -- Translation id - foreign key to translations.translation_id
       translator_translation_id int not null
 ) /*$wgDBTableOptions*/;
 
+DROP TABLE IF EXISTS /*_*/drafts;
+CREATE TABLE /*_*/drafts (
+      -- Draft Id - foreign key to translations.translation_id
+      draft_id int primary key not null,
+      -- Draft save timestamp
+      draft_timestamp  varchar(14) binary not null,
+      -- Translation draft content
+      draft_content mediumblob
+) /*$wgDBTableOptions*/;
+
+
+CREATE UNIQUE INDEX /*i*/translation_pair ON /*_*/translations ( 
translation_source_title, translation_source_language, 
translation_target_language);
+
 CREATE UNIQUE INDEX /*i*/translation_translators ON /*_*/translators 
(translator_user_id, translator_translation_id);

-- 
To view, visit https://gerrit.wikimedia.org/r/172528
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: If97c80f6df69dab9eac68faf7d9a4fc0097df837
Gerrit-PatchSet: 14
Gerrit-Project: mediawiki/extensions/ContentTranslation
Gerrit-Branch: master
Gerrit-Owner: Santhosh <[email protected]>
Gerrit-Reviewer: KartikMistry <[email protected]>
Gerrit-Reviewer: Nemo bis <[email protected]>
Gerrit-Reviewer: Nikerabbit <[email protected]>
Gerrit-Reviewer: Santhosh <[email protected]>
Gerrit-Reviewer: Siebrand <[email protected]>
Gerrit-Reviewer: Springle <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to