[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
yvi.
Files modified:
http://bugs.dwscoalition.org/show_bug.cgi?id=1365
Add ability to properly merge tags. Yay\!
Patch by
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 = ' ';
@@ -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.
--------------------------------------------------------------------------------

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