mark: A photo of Mark kneeling on top of the Taal Volcano in the Philippines. It was a long hike. (Default)
Mark Smith ([staff profile] mark) wrote in [site community profile] changelog2010-05-02 04:16 am

[dw-free] Add support for merging tags

[commit: http://hg.dwscoalition.org/dw-free/rev/1ce0dd56861d]

http://bugs.dwscoalition.org/show_bug.cgi?id=1365

Add ability to properly merge tags. Yay\!

Patch by [personal profile] yvi.

Files modified:
  • bin/upgrading/en.dat
  • cgi-bin/LJ/Tags.pm
  • htdocs/js/tags.js
  • htdocs/manage/tags.bml
  • htdocs/manage/tags.bml.text
--------------------------------------------------------------------------------
diff -r 7c0522b8609c -r 1ce0dd56861d bin/upgrading/en.dat
--- a/bin/upgrading/en.dat	Sun May 02 00:20:20 2010 +0000
+++ b/bin/upgrading/en.dat	Sun May 02 04:15:53 2010 +0000
@@ -3480,9 +3480,13 @@ taglib.error.add=You are not allowed to 
 
 taglib.error.delete=You are not allowed to delete tags from entries for this journal; the entry is still tagged with [[tags]].
 
+taglib.error.exists=The tag name '[[tagname]]' is already in use and cannot be renamed to. You can merge the tags instead.
+
 taglib.error.invalid=The following tag name is invalid: [[tagname]]
 
-taglib.error.nomerge=The tag name '[[tagname]]' is already in use.  Merging tags is not supported at this time.
+taglib.error.mergenoname=You did not provide a tag name you want to merge to.
+
+taglib.error.mergetoexisting=The tag name '[[tagname]]' is already in use, but you did not select it. Please select the tag or merge to a different tag name.
 
 taglib.error.toomany=This would make you exceed your maximum of [[max]] tags.  Please remove some and try again.
 
diff -r 7c0522b8609c -r 1ce0dd56861d cgi-bin/LJ/Tags.pm
--- a/cgi-bin/LJ/Tags.pm	Sun May 02 00:20:20 2010 +0000
+++ b/cgi-bin/LJ/Tags.pm	Sun May 02 04:15:53 2010 +0000
@@ -1181,7 +1181,7 @@ sub delete_usertag {
 # <LJFUNC>
 # name: LJ::Tags::rename_usertag
 # class: tags
-# des: Deletes a tag for a user, and all mappings.
+# des: Renames a tag for a user
 # args: uobj, type, tag, newname, error_ref (optional)
 # des-uobj: User object to delete tag on.
 # des-type: Either 'id' or 'name', indicating the type of the third parameter.
@@ -1193,8 +1193,6 @@ sub delete_usertag {
 # </LJFUNC>
 sub rename_usertag {
     return undef unless LJ::is_enabled('tags');
-
-    # FIXME/TODO: make this function do merging?
 
     my $u = LJ::want_user(shift);
     return undef unless $u;
@@ -1232,9 +1230,10 @@ sub rename_usertag {
     return undef unless $newkwid;
 
     # see if the tag we're renaming TO already exists as a keyword,
-    # if so, don't allow the rename because we don't do merging (yet)
-    my $tags = LJ::Tags::get_usertags($u);
-    return $err->( LJ::Lang::ml( 'taglib.error.nomerge', { tagname => $newname } ) )
+    # if so, error and suggest merging the tags
+    # FIXME: ask user to merge and then merge
+    my $tags = LJ::Tags::get_usertags( $u );
+    return $err->( LJ::Lang::ml( 'taglib.error.exists', { tagname => LJ::ehtml( $oldkw ) } ) )
         if $tags->{$newkwid};
 
     # escape sub
@@ -1273,6 +1272,206 @@ sub rename_usertag {
     LJ::Tags::reset_cache($u);
     LJ::Tags::reset_cache($u => \@jitemids);
     return 1;
+}
+
+# <LJFUNC>
+# name: LJ::Tags::merge_usertags
+# class: tags
+# des: Merges usertags
+# args: uobj, newname, error_ref, oldnames
+# des-uobj: User object to merge tag on.
+# des-newname: new name for these tags, might be one that already exists
+# des-error_ref: ref to scalar to return error messages in.
+# des-oldnames: array of tags that need to be merged
+# returns: undef on error, 1 for success
+# </LJFUNC>
+sub merge_usertags {
+    return undef unless LJ::is_enabled( 'tags' );
+
+    my $u = LJ::want_user( shift );
+    return undef unless $u;
+    my ( $merge_to, $ref, @merge_from ) = @_;
+    my $userid = $u->userid;
+    return undef unless $userid;
+
+    # error output
+    my $err = sub {
+        my $err_ref = $ref && ref $ref eq 'SCALAR' ? $ref : \"";
+        $$err_ref = shift() || "Unspecified error";
+        return undef;
+    };
+
+    # check whether we have a new name
+    return $err->( LJ::Lang::ml( 'taglib.error.mergenoname') )
+        unless $merge_to;
+
+    # check whether new tag is valid
+    my $newname = LJ::Tags::validate_tag( $merge_to );
+    return $err->( LJ::Lang::ml( 'taglib.error.invalid', { tagname => LJ::ehtml( $merge_to ) } ) )
+        unless $newname;
+
+    # check whether tag to merge to already exists
+    # if it exists, but isn't selected for merging, throw error as this could be a mistake
+    my $tags = LJ::Tags::get_usertags( $u );
+    my $exists = $tags->{$u->get_keyword_id( $newname )} ? 1 : 0;
+    my %merge_from = map { $_ => 1 } @merge_from;
+    return $err->( LJ::Lang::ml( 'taglib.error.mergetoexisting', { tagname => LJ::ehtml( $merge_to ) } ) )
+        if $exists && ! %merge_from->{$merge_to};
+
+    # if necessary, create new tag id
+    my $merge_to_id;
+    if ( $exists ) {
+        $merge_to_id = $u->get_keyword_id( $newname );
+    } else {
+        my $merge_to_ids = LJ::Tags::create_usertag( $u, $newname, { display => 1 } );
+        $merge_to_id = $merge_to_ids->{$newname};
+    }
+
+    # get keyword ids of tags to merge - take out the existing one if there is one
+    my @merge_from_ids;
+    foreach my $tagname ( @merge_from ) {
+        my $val = LJ::Tags::validate_tag( $tagname );
+        return $err->( LJ::Lang::ml( 'taglib.error.invalid', { tagname => LJ::ehtml( $tagname ) } ) )
+            unless $val;
+        my $kwid = $u->get_keyword_id( $val, 0 );
+        push @merge_from_ids, $kwid unless $kwid eq $merge_to_id;
+    }
+
+    # rollback if we encounter any errors in the upcoming database transactions
+    my $rollback = sub {
+        die $u->errstr unless $u->rollback;
+        return undef;
+    };
+
+    # begin transaction
+    $u->begin_work;
+
+    # get the entry ids of entries the tag is already on if it exists
+    my @merge_to_jitemids;
+    if ( $exists ) {
+        my $sth = $u->prepare( 'SELECT jitemid FROM logtags WHERE journalid= ? AND kwid= ?' );
+        return $rollback->() if $u->err || ! $sth;
+        $sth->execute( $userid, $merge_to_id );
+        return $rollback->() if $sth->err;
+
+        push @merge_to_jitemids, $_
+            while $_ = $sth->fetchrow_array;
+    }
+
+    # getting the entry ids the tag might need to be added to (might because if we are merging to an existing tag, 
+    # we need to take out the entries that already have both a tag we are merging from and the tag we are merging to)
+    my $sth = $u->prepare( "SELECT DISTINCT jitemid FROM logtags WHERE journalid= ? AND kwid IN (" . join( ", ", ( "?" ) x @merge_from_ids ) . ")" );
+    return $rollback->() if $u->err || ! $sth;
+    $sth->execute( $userid, @merge_from_ids );
+    return $rollback->() if $sth->err;
+
+    # jitemids of all entries the tag needs to be added to, taking out the ones it is already on
+    my @jitemids;
+    if ( $exists ) {
+        my %merge_to_jitemids = map { $_ => 1 } @merge_to_jitemids;
+        while ( my $jitemid = $sth->fetchrow_array ) {
+            push @jitemids, $jitemid unless %merge_to_jitemids->{$jitemid};
+        }
+    } else {
+        push @jitemids, $_
+            while $_ = $sth->fetchrow_array;
+    }
+
+    # now we do the actual database updates to logtags, logtagsrecent, usertags, and logkwsum:
+
+    # add the tag to all entries we need to change, in both logtags and logtagsrecent
+    if ( @jitemids ) {
+        foreach my $jitemid ( @jitemids ) {
+            my $sth = $u->prepare( "INSERT INTO logtags (journalid, jitemid, kwid) VALUES ( ?, ?, ? )");
+            return $rollback->() if $u->err || ! $sth;
+            $sth->execute( $userid, $jitemid, $merge_to_id );
+            return $rollback->() if $sth->err;
+
+            my $sth = $u->prepare( "INSERT INTO logtagsrecent (journalid, jitemid, kwid) VALUES ( ?, ?, ? )");
+            return $rollback->() if $u->err || ! $sth;
+            $sth->execute( $userid, $jitemid, $merge_to_id );
+            return $rollback->() if $sth->err;
+        }
+    }
+
+    # if the tag already existed before, it already has entries in logkwsum, which we delete now
+    if ( $exists ) {
+        $u->do("DELETE FROM logkwsum WHERE journalid = ? AND kwid = ? " , undef, $userid, $merge_to_id );
+        return $rollback->() if $u->err;
+    }
+
+    # while we previously only needed the jitemids of the entries we needed to add the tag to, we now need all the ones it is a tag on after the transaction
+    # including the one it was already on before the merge
+    my $sth = $u->prepare( "SELECT jitemid FROM logtags WHERE journalid= ? AND kwid= ?" );
+    return $rollback->() if $u->err || ! $sth;
+    $sth->execute( $userid, $merge_to_id );
+    return $rollback->() if $sth->err;
+
+    # we need all jitemids in an array for later cache clearing
+    my @jitemids;
+    while ( my $itemid = $sth->fetchrow_array ) {
+        push @jitemids, $itemid;
+    }
+
+    # get security of entries this new tag is now on, so we can accurately update logkwsum
+    # this can only get executed if the tags we are merging are actually in use on entries
+    # since we don't need logkwsum entries for tags that exist and are not used on entries, we can just skip this for them
+    if ( @jitemids ) {
+        my $sth = $u->prepare( "SELECT security, allowmask FROM log2 WHERE journalid=? AND jitemid IN (" . join( ", ", ( "?" ) x @jitemids ) . ")" );
+        return $rollback->() if $u->err || ! $sth;
+
+        $sth->execute( $userid, @jitemids );
+        return $rollback->() if $sth->err;
+
+        # updating security counts: create hash for storing security values and initialize with zeros
+        my $public_mask = 1 << 63;
+        my %securities = (
+            $public_mask => 0,
+            0 => 0,
+            1 => 0,
+            2 => 0,
+        );
+
+        # count securities; if the security isn't public or private and the allowmask isn't 1, the entry is set to trusted
+        while ( my ( $security, $allowmask ) = $sth->fetchrow_array ) {
+            if ( $security eq 'public' ) {
+                $securities{$public_mask}++;
+            } elsif ( $security eq 'private' ) {
+                $securities{0}++;
+            } elsif ( $allowmask == 1 ) {
+                $securities{1}++;
+            } else {
+                $securities{2}++;
+            }
+        }
+
+        # write to logkwsum
+        while ( my ( $sec, $value ) = each %securities ) {
+            unless ( $value == 0 ) {
+                $u->do( "INSERT INTO logkwsum (journalid, kwid, security, entryct) VALUES (?, ?, ?, ?)",
+                    undef, $userid, $merge_to_id, $sec, $value );
+                return $rollback->() if $u->err;
+            }
+        }
+    }
+
+    # delete other tags from database and entries 
+    foreach my $table ( qw( usertags logtags logtagsrecent logkwsum ) ) {
+        my $sth = $u->prepare( "DELETE FROM $table WHERE journalid = ? AND kwid IN (" . join( ", ", ( "?" ) x @merge_from_ids ) . ")" );
+        return $rollback->() if $u->err || ! $sth;
+
+        $sth->execute( $userid, @merge_from_ids );
+        return $rollback->() if $sth->err;
+    }
+
+    # done with the updates, commit
+    die $u->errstr unless $u->commit;
+
+    # reset cache on all changed entries
+    LJ::Tags::reset_cache( $u );
+    LJ::Tags::reset_cache( $u => \@jitemids );
+
+    return 1;    
 }
 
 # <LJFUNC>
diff -r 7c0522b8609c -r 1ce0dd56861d htdocs/js/tags.js
--- a/htdocs/js/tags.js	Sun May 02 00:20:20 2010 +0000
+++ b/htdocs/js/tags.js	Sun May 02 04:15:53 2010 +0000
@@ -9,20 +9,36 @@ function initTagPage()
     if (list) tagselect(list);
 }
 
-function toggle_actions(enable, just_rename)
+function toggle_actions( selected_num )
 {
     var form = document.getElementById("tagform");
     if (! form) return;
 
     // names of form elements to disable/enable
     // on item selections
-    var toggle_elements = new Array("rename", "rename_field", "delete", "show posts");
+    var toggle_elements_disabled = [], toggle_elements_enabled = [];
+    switch ( selected_num ) {
+        case 0:
+            toggle_elements_disabled = ["rename", "rename_field", "merge", "merge_field", "delete", "show posts"];
+            break;
+        case 1:
+            toggle_elements_enabled = ["rename", "rename_field", "delete", "show posts"];
+            toggle_elements_disabled = ["merge", "merge_field"];
+            break;
+        default:
+            toggle_elements_enabled = ["merge", "merge_field", "delete", "show posts"];
+            toggle_elements_disabled = ["rename", "rename_field"];
+            break;
+    }
 
-    for ( $i = 0; $i < toggle_elements.length; $i++ ) {
-        var ele = form.elements[ toggle_elements[$i] ];
-        if (just_rename && $i > 1) continue;  // FIXME: remove after merge is decided
-        ele.disabled = ! enable;
+    for ( i = 0; i < toggle_elements_disabled.length; i++ ) {
+        form.elements[toggle_elements_disabled[i]].disabled = true;
     }
+
+    for ( i = 0; i < toggle_elements_enabled.length; i++ ) {
+        form.elements[toggle_elements_enabled[i]].disabled = false;
+    }
+
 }
 
 function tagselect(list)
@@ -49,33 +65,28 @@ function tagselect(list)
 
     var tagfield   = document.getElementById("selected_tags");
     var tagprops   = document.getElementById("tag_props");
-    var rename_btn = form.elements[ "rename" ];
-    if (! tagfield || ! tagprops || ! rename_btn) return;
+    if (! tagfield || ! tagprops ) return;
 
     // reset any 'red' fields
     reset_field( form.elements[ "rename_field" ]);
     reset_field( form.elements[ "add_field" ]);
 
+    toggle_actions(selected_num);
+
     // no selections
     if (! selected_num) {
-        toggle_actions(false);
-        rename_btn.value = ml.rename_btn;
+        toggle_actions(0);
         show_props(tagprops);
     } else {
-        toggle_actions(true);
         tagfield.innerHTML = selected.join(", ");
 
         // exactly one selection
         if (selected_num == 1) {
-            rename_btn.value = ml.rename_btn;
             show_props(tagprops, selected_id);
         }
 
         // multiple items selected
         else {
-            // FIXME: enable after merging is decided
-            //rename_btn.value = "Merge";
-            toggle_actions(false, 1); // FIXME: delete after merging is decided
             show_props(tagprops);
         }
     }
diff -r 7c0522b8609c -r 1ce0dd56861d htdocs/manage/tags.bml
--- a/htdocs/manage/tags.bml	Sun May 02 00:20:20 2010 +0000
+++ b/htdocs/manage/tags.bml	Sun May 02 04:15:53 2010 +0000
@@ -42,6 +42,7 @@ body<=
     ml.security_label = "$ML{'.label.security'}";
     ml.na_label = "$ML{'.label.notapplicable'}";
     ml.rename_btn = "$ML{'.button.rename'}";
+    ml.merge_btn = "$ML{'.button.merge'}";
 
 </script>
 HEAD
@@ -79,9 +80,23 @@ HEAD
             if ( $new_tag =~ /,/ ) {
                 $ret .= "<?errorbar $ML{'.error.rename.multiple'} errorbar?>";
             } else {
-                # FIXME: merge support later
                 my $tagerr = "";
                 my $rv = LJ::Tags::rename_usertag( $u, 'name', $tagnames[0], $new_tag, \$tagerr );
+                $ret .= "<?errorbar $tagerr errorbar?>" unless $rv;
+            }
+        }
+
+        if ( $POST{merge} ) {
+            my @tagnames = map { s/\d+_//; $_; } split /\0/, $POST{tags};
+
+            # get the new name for the tags
+            my $new_tagname = LJ::trim( $POST{merge_field} );
+
+            if ( $new_tagname =~ /,/ ) {
+                $ret .= "<?errorbar $ML{'.error.rename.multiple'} errorbar?>";
+            } else {
+                my $tagerr = "";
+                my $rv = LJ::Tags::merge_usertags( $u, $new_tagname, \$tagerr, @tagnames );
                 $ret .= "<?errorbar $tagerr errorbar?>" unless $rv;
             }
         }
@@ -158,6 +173,7 @@ HEAD
         rename => $ML{'.hint.rename'},
         delete => $ML{'.hint.delete'},
         entries => $ML{'.hint.entries'},
+        merge => $ML{'.hint.merge'},
     };
 
     my $sp = '&nbsp;&nbsp;';
@@ -275,6 +291,27 @@ HEAD
          );
         $ret .= '<br /><br />';
 
+        $ret .= LJ::html_text(
+            {
+                 name    => 'merge_field',
+                 size    => 30,
+                 class   => 'tagfield',
+                 onClick => 'reset_field(this)',
+             }
+          );
+         $ret .= $sp;
+         my $merge_conf = LJ::ejs( $ML{'.confirm.merge'} );
+         $ret .= LJ::html_submit(
+             'merge', $ML{'.button.merge'},
+             {
+                 class   => 'btn',
+                 title   => $mo->{merge},
+                 onClick => "return confirm('$merge_conf')",
+              }
+          );
+
+         $ret .= '<br /><br />';
+
         my $del_conf = LJ::ejs( $ML{'.confirm.delete'} );
         $ret .= LJ::html_submit(
             'delete', $ML{'.button.delete'},
diff -r 7c0522b8609c -r 1ce0dd56861d htdocs/manage/tags.bml.text
--- a/htdocs/manage/tags.bml.text	Sun May 02 00:20:20 2010 +0000
+++ b/htdocs/manage/tags.bml.text	Sun May 02 04:15:53 2010 +0000
@@ -2,6 +2,8 @@
 .addnew=Add new tags here!
 
 .button.delete=Remove selected tag(s)
+
+.button.merge=Merge selected tags
 
 .button.rename=Rename
 
@@ -10,6 +12,8 @@
 .button.show=Show journal entries
 
 .confirm.delete=Are you sure you want to remove the selected tags? This operation is not reversible.
+
+.confirm.merge=Are you sure you want to merge the selected tags? This operation is not reversible.
 
 .error.invalidsettings=Invalid selection for tag permission settings.
 
@@ -20,6 +24,8 @@
 .hint.delete=Remove all references to the selected tag(s).
 
 .hint.entries=Display journal entries marked with the selected tag(s).
+
+.hint.merge=Merge several tags into one.
 
 .hint.rename=Change a tag name.
 
--------------------------------------------------------------------------------
yvi: (Dreamwidth - Badass Dreamwidth dev)

[personal profile] yvi 2010-05-02 07:47 am (UTC)(link)
*hyperventilates a bit*

!
turlough: Gabe Saporta doing thumbs-up ((cs) gabe approves)

[personal profile] turlough 2010-05-02 02:25 pm (UTC)(link)
Yay!!
wenchpixie: (GG Dean Drunk Yay Arms)

[personal profile] wenchpixie 2010-05-04 11:26 am (UTC)(link)
Oh AWESOME, I cannot WAIT for this to be committed :D.

Thank you Dwth, for once again anticipating my needs before I knew I had them ♥