<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[GlotPress][867] trunk: New feature: Project Branching</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="https://glotpress.trac.wordpress.org/changeset/867">867</a></dd>
<dt>Author</dt> <dd>yoavf</dd>
<dt>Date</dt> <dd>2014-03-26 08:04:46 +0000 (Wed, 26 Mar 2014)</dd>
</dl>

<h3>Log Message</h3>
<pre>New feature: Project Branching

Allow a project to be branched into another duplicate project.
See <a href="http://glotpress.trac.wordpress.org/ticket/65">#65</a></pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkgpincludesrouterphp">trunk/gp-includes/router.php</a></li>
<li><a href="#trunkgpincludesroutesprojectphp">trunk/gp-includes/routes/project.php</a></li>
<li><a href="#trunkgpincludestemplatephp">trunk/gp-includes/template.php</a></li>
<li><a href="#trunkgpincludesthingsprojectphp">trunk/gp-includes/things/project.php</a></li>
<li><a href="#trunktteststests_thingstest_thing_projectphp">trunk/t/tests/tests_things/test_thing_project.php</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunkgptemplatesprojectbranchphp">trunk/gp-templates/project-branch.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkgpincludesrouterphp"></a>
<div class="modfile"><h4>Modified: trunk/gp-includes/router.php (866 => 867)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/gp-includes/router.php     2014-03-25 08:53:43 UTC (rev 866)
+++ trunk/gp-includes/router.php        2014-03-26 08:04:46 UTC (rev 867)
</span><span class="lines">@@ -76,6 +76,8 @@
</span><span class="cx">                  "post:/$project/-mass-create-sets" => array('GP_Route_Project', 'mass_create_sets_post'),
</span><span class="cx">                  "post:/$project/-mass-create-sets/preview" => array('GP_Route_Project', 'mass_create_sets_preview_post'),
</span><span class="cx"> 
</span><ins>+                       "get:/$project/-branch" => array('GP_Route_Project', 'branch_project_get'),
+                       "post:/$project/-branch" => array('GP_Route_Project', 'branch_project_post'),
</ins><span class="cx"> 
</span><span class="cx">                  "get:/$projects" => array('GP_Route_Project', 'index'),
</span><span class="cx">                  "get:/$projects/-new" => array('GP_Route_Project', 'new_get'),
</span></span></pre></div>
<a id="trunkgpincludesroutesprojectphp"></a>
<div class="modfile"><h4>Modified: trunk/gp-includes/routes/project.php (866 => 867)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/gp-includes/routes/project.php     2014-03-25 08:53:43 UTC (rev 866)
+++ trunk/gp-includes/routes/project.php        2014-03-26 08:04:46 UTC (rev 867)
</span><span class="lines">@@ -253,4 +253,48 @@
</span><span class="cx">          header('Content-Type: application/json');
</span><span class="cx">          echo json_encode( $project->set_difference_from( $other_project ) );
</span><span class="cx">  }
</span><ins>+
+       function branch_project_get( $project_path ) {
+               $project = GP::$project->by_path( $project_path );
+               if ( !$project ) {
+                       $this->die_with_404();
+               }
+               if ( $this->cannot_and_redirect( 'write', 'project', $project->id ) ) {
+                       return;
+               }
+
+               $this->tmpl( 'project-branch', get_defined_vars() );
+       }
+
+
+       function branch_project_post( $project_path ) {
+               $post = gp_post( 'project' );
+               $project = GP::$project->by_path( $project_path );
+               if ( !$project ) {
+                       $this->die_with_404();
+               }
+
+               $parent_project_id = gp_array_get( $post, 'parent_project_id', null );
+
+               if ( $this->cannot_and_redirect( 'write', 'project', $parent_project_id ) ) {
+                       return;
+               }
+
+               $new_project_data = new GP_Project( $post );
+               if ( $this->invalid_and_redirect( $new_project_data ) ){
+                       return;
+               }
+
+               $new_project = GP::$project->create_and_select( $new_project_data );
+
+               if ( !$new_project ) {
+                       $new_project = new GP_Project();
+                       $this->errors[] = __('Error in creating project!');
+                       $this->tmpl( 'project-branch', get_defined_vars() );
+               } else {
+                       $new_project->duplicate_project_contents_from( $project );
+               }
+
+               $this->redirect( gp_url_project( $new_project ) );
+       }
</ins><span class="cx"> }
</span></span></pre></div>
<a id="trunkgpincludestemplatephp"></a>
<div class="modfile"><h4>Modified: trunk/gp-includes/template.php (866 => 867)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/gp-includes/template.php   2014-03-25 08:53:43 UTC (rev 866)
+++ trunk/gp-includes/template.php      2014-03-26 08:04:46 UTC (rev 867)
</span><span class="lines">@@ -282,6 +282,7 @@
</span><span class="cx">          gp_link_get( gp_url_project( '', '-new', array('parent_project_id' => $project->id) ), __('New Sub-Project') ),
</span><span class="cx">          gp_link_get( gp_url( '/sets/-new', array( 'project_id' => $project->id ) ), __('New Translation Set') ),
</span><span class="cx">          gp_link_get( gp_url_project( $project, array( '-mass-create-sets' ) ), __('Mass-create Translation Sets') ),
</span><ins>+               gp_link_get( gp_url_project( $project, '-branch'), __('Branch Project') ),
</ins><span class="cx">           gp_link_with_ays_get( gp_url_project( $project, '-delete'), __('Delete Project'), array( 'ays-text' => 'Do you really want to delete this project?' ) )
</span><span class="cx">  );
</span><span class="cx"> 
</span></span></pre></div>
<a id="trunkgpincludesthingsprojectphp"></a>
<div class="modfile"><h4>Modified: trunk/gp-includes/things/project.php (866 => 867)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/gp-includes/things/project.php     2014-03-25 08:53:43 UTC (rev 866)
+++ trunk/gp-includes/things/project.php        2014-03-26 08:04:46 UTC (rev 867)
</span><span class="lines">@@ -162,5 +162,68 @@
</span><span class="cx">          }
</span><span class="cx">          return compact( 'added', 'removed' );
</span><span class="cx">  }
</span><ins>+
+       function copy_sets_and_translations_from( $source_project_id ) {
+               $sets = GP::$translation_set->by_project_id( $source_project_id );
+
+               foreach( $sets as $to_add ) {
+                       $new_set = GP::$translation_set->create( array( 'project_id' => $this->id, 'name' => $to_add->name, 'locale' => $to_add->locale, 'slug' => $to_add->slug ) );
+                       if ( ! $new_set  ) {
+                               $this->errors[] = __( 'Couldn&#8217;t add translation set named %s', esc_html( $to_add->name ) );
+                       } else {
+                               //Duplicate translations
+                               $new_set->copy_translations_from( $to_add->id );
+                       }
+               }
+       }
+
+       function copy_originals_from( $source_project_id ) {
+               global $gpdb;
+               return $this->query("
+                       INSERT INTO $gpdb->originals (
+                               `project_id`, `context`, `singular`, `plural`, `references`, `comment`, `status`, `priority`, `date_added`
+                       )
+                       SELECT
+                               %s AS `project_id`, `context`, `singular`, `plural`, `references`, `comment`, `status`, `priority`, `date_added`
+                       FROM $gpdb->originals WHERE project_id = %s", $this->id, $source_project_id
+               );
+       }
+
+       /**
+        * Gives an array of project objects starting from the current project children
+        * then its grand children etc
+        *
+        * @return array
+        */
+       function inclusive_sub_projects() {
+               $sub_projects = $this->sub_projects();
+               foreach ( $sub_projects as $sub ) {
+                       $sub_projects = array_merge( $sub_projects, $sub->inclusive_sub_projects() );
+               }
+
+               return $sub_projects;
+       }
+
+       function duplicate_project_contents_from( $source_project ){
+               $source_sub_projects = $source_project->inclusive_sub_projects();
+
+               //Duplicate originals, translations sets and translations for the root project
+               $this->copy_originals_from( $source_project->id ) ;
+               $this->copy_sets_and_translations_from( $source_project->id );
+
+               //Duplicate originals, translations sets and translations for the child projects
+               $running_parent_project_id = $this->id;
+               foreach ( $source_sub_projects as $sub ) {
+
+                       $sub->parent_project_id = $running_parent_project_id;
+                       $copy = GP::$project->create( $sub );
+
+                       $copy->copy_originals_from( $sub->id );
+                       $copy->copy_sets_and_translations_from( $sub->id );
+
+                       $running_parent_project_id = $copy->id;
+               }
+       }
+
</ins><span class="cx"> }
</span><span class="cx"> GP::$project = new GP_Project();
</span><span class="cx">\ No newline at end of file
</span></span></pre></div>
<a id="trunkgptemplatesprojectbranchphp"></a>
<div class="addfile"><h4>Added: trunk/gp-templates/project-branch.php (0 => 867)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/gp-templates/project-branch.php                            (rev 0)
+++ trunk/gp-templates/project-branch.php       2014-03-26 08:04:46 UTC (rev 867)
</span><span class="lines">@@ -0,0 +1,29 @@
</span><ins>+<?php
+gp_title( sprintf( __( 'Branch Project %s &lt; GlotPress' ),  $project->name ) );
+gp_breadcrumb_project( $project );
+gp_tmpl_header();
+?>
+<h2><?php echo wptexturize( sprintf( __('Branch project "%s"'), esc_html( $project->name ) ) ); ?></h2>
+<p><?php _e('Here you can branch out this project: everything will be duplicated into a new project for you.'); ?></p>
+<form action="<?php echo esc_url( gp_url_current() ); ?>" method="post">
+       <dt><label for="project[name]"><?php _e('New branch name'); ?></label></dt>
+       <dd><input type="text" name="project[name]" value="" placeholder="type tag project name here" id="project[name]"></dd>
+
+       <!-- TODO: make slug edit WordPress style -->
+       <dt><label for="project[slug]"><?php _e('New Slug'); ?></label></dt>
+       <dd>
+               <input type="text" name="project[slug]" value="" id="project[slug]">
+               <small><?php _e('If you leave the slug empty, it will be derived from the name.'); ?></small>
+       </dd>
+       <dt><label for="project[source_url_template]"><?php _e('Source file URL'); ?></label></dt>
+       <dd>
+               <input type="text" value="<?php echo esc_html( $project->source_url_template ); ?>" name="project[source_url_template]" id="project[source_url_template]" style="width: 30em;" />
+               <span class="ternary"><?php _e('Public URL to a source file in the project. You can use <code>%file%</code> and <code>%line%</code>. Ex. <code>http://trac.example.org/browser/%file%#L%line%</code>'); ?></span>
+       </dd>
+       <div id="preview"></div>
+       <input type="hidden" value="<?php echo esc_html( $project->description ); ?>" name="project[description]" id="project[description]" />
+       <input type="hidden" value="<?php echo esc_html( $project->parent_project_id ); ?>" name="project[parent_project_id]" id="project[parent_project_id]" />
+       <p><input type="submit" name="submit" value="<?php echo esc_attr( __('Branch project') ); ?>" id="submit" /></p>
+</form>
+
+<?php gp_tmpl_footer();
</ins></span></pre></div>
<a id="trunktteststests_thingstest_thing_projectphp"></a>
<div class="modfile"><h4>Modified: trunk/t/tests/tests_things/test_thing_project.php (866 => 867)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/t/tests/tests_things/test_thing_project.php        2014-03-25 08:53:43 UTC (rev 866)
+++ trunk/t/tests/tests_things/test_thing_project.php   2014-03-26 08:04:46 UTC (rev 867)
</span><span class="lines">@@ -110,4 +110,113 @@
</span><span class="cx">          $this->assertEquals( $s1->id, $difference['removed'][0]->id );
</span><span class="cx">  }
</span><span class="cx"> 
</span><ins>+       function test_copy_originals_from() {
+               $s1 = $this->factory->translation_set->create_with_project_and_locale( array( 'locale' => 'bg' ), array( 'name' => 'P1' ) );
+               $s2 = $this->factory->translation_set->create_with_project_and_locale( array( 'locale' => 'nl' ), array( 'name' => 'P2' ) );
+
+               $this->factory->translation->create_with_original_for_translation_set( $s1 );
+
+               $s2->project->copy_originals_from( $s1->project->id );
+
+               $s1_original = array_shift( GP::$original->by_project_id( $s1->project->id ) );
+               $s2_original = array_shift( GP::$original->by_project_id( $s2->project->id ) );
+
+               $this->assertNotEquals( $s1_original->id, $s2_original->id );
+               $this->assertNotEquals( $s1_original->project_id, $s2_original->project_id );
+               $this->assertEqualFields( $s2_original,
+                       array( 'singular' => $s1_original->singular, 'plural' => $s1_original->plural, 'references' => $s1_original->references, 'comment' =>$s1_original->comment, 'status' =>$s1_original->status, 'date_added' => $s1_original->date_added )
+               );
+       }
+
+       function test_sets_in_copy_sets_and_translations_from() {
+               $s1 = $this->factory->translation_set->create_with_project_and_locale( array( 'locale' => 'bg' ), array( 'name' => 'P1' ) );
+               $this->factory->translation->create_with_original_for_translation_set( $s1 );
+
+               $branch = $this->factory->project->create( array( 'name' => 'branch' ) );
+               $branch->copy_sets_and_translations_from( $s1->project->id );
+
+               $difference = $branch->set_difference_from( $s1->project );
+
+               $this->assertEmpty( $difference['added'] );
+               $this->assertEmpty( $difference['removed'] );
+
+       }
+
+       function test_translations_in_copy_sets_and_translations_from() {
+               $original = $this->factory->translation_set->create_with_project_and_locale( array( 'locale' => 'bg' ), array( 'name' => 'P1' ) );
+               $this->factory->translation->create_with_original_for_translation_set( $original );
+
+               $copy = $this->factory->project->create( array( 'name' => 'branch' ) );
+               $copy->copy_sets_and_translations_from( $original->project->id );
+
+               $copy_set = array_shift( GP::$translation_set->by_project_id( $copy->id ) );
+
+               $original_translation = array_shift( GP::$translation->find( array( 'translation_set_id' => $original->id ) ) );
+               $copy_translation = array_shift( GP::$translation->find( array( 'translation_set_id' => $copy_set->id ) ) );
+
+               $this->assertEqualFields( $copy_translation,
+                       array( 'translation_0' => $original_translation->translation_0, 'date_added' => $original_translation->date_added, 'original_id' => $original_translation->original_id )
+               );
+
+       }
+
+       function test_branching_translation_sets(){
+               $root_set = $this->factory->translation_set->create_with_project_and_locale( array( 'locale' => 'bg' ), array( 'name' => 'root' ) );
+               $root = $root_set->project;
+
+               $sub_set = $this->factory->translation_set->create_with_project_and_locale( array( 'locale' => 'bg' ), array( 'name' => 'SubSub', 'parent_project_id' => $root->id ) );
+               $sub = $sub_set->project;
+
+               $subsub_set = $this->factory->translation_set->create_with_project_and_locale( array( 'locale' => 'bg' ), array( 'name' => 'SubSub', 'parent_project_id' => $sub->id ) );
+               $subsub = $subsub_set->project;
+
+               $this->factory->translation->create_with_original_for_translation_set( $root_set );
+               $this->factory->translation->create_with_original_for_translation_set( $sub_set );
+               $this->factory->translation->create_with_original_for_translation_set( $subsub_set );
+
+               $branch = $this->factory->project->create( array( 'name' => 'branch' ) );
+               $branch->duplicate_project_contents_from( $root );
+
+               $branch_sub = array_shift( $branch->sub_projects() );
+               $branch_subsub = array_shift( $branch_sub->sub_projects() );
+
+               $difference_root = $root->set_difference_from( $branch );
+               $difference_sub = $sub->set_difference_from( $branch_sub );
+               $difference_subsub = $subsub->set_difference_from( $branch_subsub );
+
+               $this->assertEmpty( $difference_root['added'] );
+               $this->assertEmpty( $difference_root['removed'] );
+
+               $this->assertEmpty( $difference_sub['added'] );
+               $this->assertEmpty( $difference_sub['removed'] );
+
+               $this->assertEmpty( $difference_subsub['added'] );
+               $this->assertEmpty( $difference_subsub['removed'] );
+       }
+
+       function test_branching_originals(){
+               $root_set = $this->factory->translation_set->create_with_project_and_locale( array( 'locale' => 'bg' ), array( 'name' => 'root' ) );
+               $root = $root_set->project;
+
+               $sub_set = $this->factory->translation_set->create_with_project_and_locale( array( 'locale' => 'bg' ), array( 'name' => 'SubSub', 'parent_project_id' => $root->id ) );
+               $sub = $sub_set->project;
+
+               $this->factory->translation->create_with_original_for_translation_set( $root_set );
+               $this->factory->translation->create_with_original_for_translation_set( $sub_set );
+
+               $branch = $this->factory->project->create( array( 'name' => 'branch' ) );
+               $branch->duplicate_project_contents_from( $root );
+
+               $branch_sub = array_shift( $branch->sub_projects() );
+
+               $originals_root = GP::$original->by_project_id( $root->id );
+               $originals_sub = GP::$original->by_project_id( $sub->id );
+
+               $originals_branch = GP::$original->by_project_id( $branch->id );
+               $originals_branch_sub = GP::$original->by_project_id( $branch_sub->id );
+
+               $this->assertEquals( count( $originals_root ), count( $originals_branch ) );
+               $this->assertEquals( count( $originals_sub ), count( $originals_branch_sub ) );
+       }
+
</ins><span class="cx"> }
</span><span class="cx">\ No newline at end of file
</span></span></pre>
</div>
</div>

</body>
</html>