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] changelog2009-11-24 08:12 pm

[dw-free] Userpic keyword renaming

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

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

Add ability to actually rename icon keywords. When you edit the keywords on
an icon, you can click a box to do a rename on those keywords, and comments
and posts you've made with the old keywords will be updated to use the new
ones.

Patch by [personal profile] allen.

Files modified:
  • bin/worker/userpic-rename
  • cgi-bin/DW/Worker/UserpicRenameWorker.pm
  • cgi-bin/LJ/Userpic.pm
  • htdocs/editpics.bml
  • htdocs/editpics.bml.text
  • htdocs/stc/editpics.css
--------------------------------------------------------------------------------
diff -r e36a761d81b3 -r be246bd19221 bin/worker/userpic-rename
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/worker/userpic-rename	Tue Nov 24 20:12:09 2009 +0000
@@ -0,0 +1,27 @@
+#!/usr/bin/perl
+#
+# bin/worker/userpic-rename
+#
+# TheSchwartz worker for renaming userpics
+#
+# Authors:
+#      Allen Petersen <allen@suberic.net>
+#
+# Copyright (c) 2009 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself. For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+
+use strict;
+use warnings;
+
+use lib "$ENV{LJHOME}/cgi-bin";
+use LJ::Worker::TheSchwartz;
+use DW::Worker::UserpicRenameWorker;
+
+foreach my $capability (DW::Worker::UserpicRenameWorker->schwartz_capabilities) {
+    schwartz_decl( $capability );
+}
+
+schwartz_work(); # Never returns.
diff -r e36a761d81b3 -r be246bd19221 cgi-bin/DW/Worker/UserpicRenameWorker.pm
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cgi-bin/DW/Worker/UserpicRenameWorker.pm	Tue Nov 24 20:12:09 2009 +0000
@@ -0,0 +1,144 @@
+#!/usr/bin/perl
+#
+# DW::Worker::UserpicRenameWorker
+#
+# TheSchwartz worker module for renaming userpics.  Called with
+# LJ::theschwartz()->insert('DW::Worker::UserpicRenameWorker', { 
+# 'uid' => $u->userid, 'keywordmap' => Storable::nfreeze(\%keywordmap);
+#
+# Authors:
+#      Allen Petersen <allen@suberic.net>
+#
+# Copyright (c) 2009 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself. For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+
+use strict;
+use warnings;
+use lib "$LJ::HOME/cgi-bin";
+
+package DW::Worker::UserpicRenameWorker;
+use base 'TheSchwartz::Worker';
+
+sub schwartz_capabilities { return ('DW::Worker::UserpicRenameWorker'); }
+
+sub keep_exit_status_for { 86400 } # 24 hours
+
+my $logpropid;
+my $talkpropid;
+
+sub work {
+    my ($class, $job) = @_;
+
+    my $arg = $job->arg;
+
+    my ($uid, $keywordmapstring) = map { delete $arg->{$_} } qw( uid keywordmap );
+
+    return $job->permanent_failure("Unknown keys: " . join(", ", keys %$arg))
+        if keys %$arg;
+    return $job->permanent_failure("Missing argument")
+        unless defined $uid && defined $keywordmapstring;
+
+    my $keywordmap = eval { Storable::thaw($keywordmapstring) } or return $job->failed("Failed to load keywordmap from arg '$keywordmapstring':  " . $@);
+
+    # get the user from the uid
+    my $u = LJ::want_user($uid) or return $job->failed("Unable to load user with uid $uid");
+
+    # only get the propids once; they're not going to change
+    unless ($logpropid && $talkpropid) {
+        $logpropid = LJ::get_prop( log => 'picture_keyword' )->{id};
+        $talkpropid = LJ::get_prop( talk => 'picture_keyword' )->{id};
+    }
+
+    # only update 1000 rows at a time.
+    my $LIMIT = 1000;
+
+    # now we go through each cluster and update the logprop2 and talkprop2
+    # tables with the new values.
+    foreach my $cluster_id (@LJ::CLUSTERS) {
+        my $dbcm = LJ::get_cluster_master($cluster_id);
+        
+        foreach my $kw (keys %$keywordmap) {
+            # find entries for clearing cache
+            my $matches = $dbcm->selectall_arrayref(q{
+                SELECT log2.journalid AS journalid, 
+                       log2.jitemid AS jitemid 
+                FROM logprop2 
+                INNER JOIN log2 
+                    ON ( logprop2.journalid = log2.journalid 
+                        AND logprop2.jitemid = log2.jitemid ) 
+                WHERE posterid = ? 
+                    AND propid=?
+                    AND value = ?
+                }, undef, $u->id, $logpropid, $kw);
+
+            # update entries
+            my $updateresults = $LIMIT;
+            while ($updateresults == $LIMIT) {
+                $updateresults = $dbcm->do( q{
+                    UPDATE logprop2 
+                    SET value = ? 
+                    WHERE propid=? 
+                        AND value = ? 
+                        AND EXISTS ( 
+                            SELECT posterid 
+                            FROM log2 
+                            WHERE log2.journalid = logprop2.journalid 
+                                AND log2.jitemid = logprop2.jitemid 
+                                AND log2.posterid = ? )
+                    LIMIT ?
+                    }, undef, $keywordmap->{$kw}, $logpropid, $kw, $u->id, $LIMIT);
+                return $job->permanent_failure($dbcm->errstr) if $dbcm->err;
+            }
+
+            # clear cache
+            foreach my $match (@$matches) {
+                LJ::MemCache::delete([ $match->[0], "logprop:" . $match->[0]. ":" . $match->[1] ]);
+            }
+            
+            # update comments
+            # find comments for clearing cache
+            $matches = $dbcm->selectall_arrayref( q{
+                SELECT talkprop2.journalid AS journalid, 
+                       talkprop2.jtalkid AS jtalkid 
+                FROM talkprop2 
+                INNER JOIN talk2 
+                    ON ( talkprop2.journalid = talk2.journalid 
+                        AND talkprop2.jtalkid = talk2.jtalkid ) 
+                WHERE posterid = ? 
+                    AND tpropid=? 
+                    AND value = ? 
+                }, undef, $u->id, $talkpropid, $kw);
+            
+            # update coments
+            $updateresults = $LIMIT;
+            while ($updateresults == $LIMIT) {
+                $updateresults = $dbcm->do( q{
+                    UPDATE talkprop2 
+                    SET value = ? 
+                    WHERE tpropid=? 
+                        AND value = ? 
+                        AND EXISTS ( 
+                            SELECT posterid 
+                            FROM talk2 
+                            WHERE talk2.journalid = talkprop2.journalid 
+                                AND talk2.jtalkid = talkprop2.jtalkid 
+                                AND talk2.posterid = ? ) 
+                    LIMIT ? 
+                    }, undef, $keywordmap->{$kw}, $talkpropid, $kw, $u->id, $LIMIT);
+                return $job->permanent_failure($dbcm->errstr) if $dbcm->err;
+            }
+
+            # clear cache
+            foreach my $match (@$matches) {
+                LJ::MemCache::delete([ $match->[0], "talkprop:" . $match->[0]. ":" . $match->[1] ]);
+            }
+        }
+    }
+
+    $job->completed;
+}
+
+1;
diff -r e36a761d81b3 -r be246bd19221 cgi-bin/LJ/Userpic.pm
--- a/cgi-bin/LJ/Userpic.pm	Tue Nov 24 20:03:33 2009 +0000
+++ b/cgi-bin/LJ/Userpic.pm	Tue Nov 24 20:12:09 2009 +0000
@@ -4,6 +4,7 @@ use Digest::MD5;
 use Digest::MD5;
 use LJ::Event::NewUserpic;
 use LJ::Constants;
+use Storable;
 
 ##
 ## Potential properties of an LJ::Userpic object
@@ -919,6 +920,97 @@ sub set_keywords {
     return 1;
 }
 
+
+# instance method:  takes two strings of comma-separated keywords, the first 
+# being the new set of keywords, the second being the old set of keywords.  
+#
+# the new keywords must be the same number as the old keywords; that is,
+# if the userpic has three keywords and you want to rename them, you must
+# rename them to three keywords (some can match).  otherwise there would be
+# some ambiguity about which old keywords should match up with the new
+# keywords.  if the number of keywords don't match, then an error is thrown 
+# and no changes are made to the keywords for this userpic.
+# 
+# all new keywords must not currently be in use; you can't rename a keyword
+# to a keyword currently mapped to another (or the same) userpic.  this will
+# result in an error and no changes made to these keywords.
+#
+# the setting of new keywords is done by Userpic->set_keywords; the remapping
+# of existing pics is done asynchonously via TheSchwartz using the
+# DW::Worker::UserpicRenameWorker.
+sub set_and_rename_keywords {
+    my $self = shift;
+    my $new_keyword_string = $_[0];
+    my $orig_keyword_string = $_[1];
+
+    my $sclient = LJ::theschwartz();
+    
+    # error out if TheSchwartz isn't available.
+    LJ::errobj("Userpic::RenameKeywords",
+               origkw    => $orig_keyword_string,
+               newkw     => $new_keyword_string)->throw unless $sclient;
+
+    my @keywords = split(',', $new_keyword_string);
+    my @orig_keywords = split(',', $orig_keyword_string);
+
+    # don't allow renames involving no-keyword (pic#0001) values
+    if ( grep ( /^\s*pic\#\d+\s*$/, @orig_keywords ) || grep ( /^\s*pic\#\d+\s*$/, @keywords )) {
+        LJ::errobj("Userpic::RenameBlankKeywords",
+                   origkw    => $orig_keyword_string,
+                   newkw     => $new_keyword_string)->throw;
+    }
+
+    # compare sizes
+    if (scalar @keywords ne scalar @orig_keywords) {
+        LJ::errobj("Userpic::MismatchRenameKeywords",
+                   origkw    => $orig_keyword_string,
+                   newkw     => $new_keyword_string)->throw;
+    }
+
+    #interleave these into a map, excluding duplicates
+    my %keywordmap;
+    foreach my $newkw (@keywords) {
+        my $origkw = shift(@orig_keywords);
+        # clear whitespace
+        $newkw =~ s/^\s+//; 
+        $newkw =~ s/\s+$//;
+        $origkw =~ s/^\s+//; 
+        $origkw =~ s/\s+$//;
+
+        $keywordmap{$origkw} = $newkw if $origkw ne $newkw;
+    }
+    
+    # make sure there is at least one change.
+    if (keys(%keywordmap)) {
+
+        #make sure that none of the target keywords already exist.
+        my $u = $self->owner;
+        foreach my $kw (keys %keywordmap) {
+            if (LJ::get_picid_from_keyword($u, $keywordmap{$kw}, -1) != -1) {
+                LJ::errobj("Userpic::RenameKeywordExisting",
+                           keyword => $keywordmap{$kw})->throw;
+            }
+        }
+        
+        # set the keywords for this userpic to the new set of keywords
+        $self->set_keywords(@keywords);
+        
+        # send to TheSchwartz to do the actual renaming 
+        my $job = TheSchwartz::Job->new_from_array(
+            'DW::Worker::UserpicRenameWorker', { 
+                'uid' => $u->userid, 
+                'keywordmap' => Storable::nfreeze(\%keywordmap) } );
+        
+        unless ($job && $sclient->insert($job)) {
+            LJ::errobj("Userpic::RenameKeywords",
+                       origkw    => $orig_keyword_string,
+                       newkw     => $new_keyword_string)->throw;
+        }
+    }
+
+    return 1;
+}
+
 sub set_fullurl {
     my ($self, $url) = @_;
     my $u = $self->owner;
@@ -988,6 +1080,45 @@ sub as_html {
                           { 'filetype' => $self->{'type'} });
 }
 
+package LJ::Error::Userpic::MismatchRenameKeywords;
+sub user_caused { 1 }
+sub fields      { qw(origkw newkw); }
+sub as_html {
+    my $self = shift;
+    return BML::ml("/editpics.bml.error.rename.mismatchedlength",
+                          { 'origkw' => $self->{'origkw'},
+                            'newkw' => $self->{'newkw'} });
+}
+
+package LJ::Error::Userpic::RenameBlankKeywords;
+sub user_caused { 1 }
+sub fields      { qw(origkw newkw); }
+sub as_html {
+    my $self = shift;
+    return BML::ml("/editpics.bml.error.rename.blankkw",
+                          { 'origkw' => $self->{'origkw'},
+                            'newkw' => $self->{'newkw'} });
+}
+
+package LJ::Error::Userpic::RenameKeywordExisting;
+sub user_caused { 1 }
+sub fields      { qw(keyword); }
+sub as_html {
+    my $self = shift;
+    return BML::ml("/editpics.bml.error.rename.keywordexists",
+                          { 'keyword' => $self->{'keyword'} });
+}
+
+package LJ::Error::Userpic::RenameKeywords;
+sub user_caused { 0 }
+sub fields      { qw(origkw newkw); }
+sub as_html {
+    my $self = shift;
+    return BML::ml("/editpics.bml.error.rename.keywords",
+                          { 'origkw' => $self->{'origkw'},
+                            'newkw' => $self->{'newkw'} });
+}
+
 package LJ::Error::Userpic::DeleteFailed;
 sub user_caused { 0 }
 
diff -r e36a761d81b3 -r be246bd19221 htdocs/editpics.bml
--- a/htdocs/editpics.bml	Tue Nov 24 20:03:33 2009 +0000
+++ b/htdocs/editpics.bml	Tue Nov 24 20:12:09 2009 +0000
@@ -78,6 +78,9 @@ use strict;
         ### save changes to existing pics
         if ($POST{'action:save'}) {
             my $refresh = update_userpics(\@userpics, $u, $err);
+
+            # push all the errors to info
+            push(@info, @errors);
 
             # reload the pictures to account for deleted
             @userpics = LJ::Userpic->load_user_userpics($u) if $refresh;
@@ -474,6 +477,8 @@ use strict;
         $body .= "</div>";
 
         $body .= "<div id='list_userpics' style='width: 98%; float: left;'>";
+        my $display_rename = LJ::theschwartz() ? 1 : 0;
+
         foreach my $pic (@userpics) {
             my $pid = $pic->id;
             
@@ -489,9 +494,20 @@ use strict;
             $body .= "<label class='left' for='kw_$pid'>$ML{'.label.keywords'}</label>\n ";
             $body .= LJ::html_text({'name' => "kw_$pid", 'class' => "text", 'id' => "kw_$pid",
                                     'value' => $keywords,
-                                    'disabled' => $pic->inactive }) . "\n";
+                                    'disabled' => $pic->inactive,
+                                    'onfocus' => $display_rename ? "\$(\'rename_div_$pid\').style.display = \'block\';" : ""
+                                   }) . "\n";
             $body .= LJ::html_hidden({ 'name' => "kw_orig_$pid",
                                        'value' => $keywords }) . "\n";
+            if ($display_rename) {
+                $body .= "<div id='rename_div_$pid' class='userpic_rename pkg'>\n";
+                $body .= "<script type='text/javascript'>document.getElementById('rename_div_$pid').style.display = 'none';</script>\n";
+                $body .= LJ::html_check({ 'type' => 'checkbox', 'name' => "rename_keyword_$pid", 'class' => "checkbox",
+                                          'id' => "rename_keyword_$pid", 'value' => 1,
+                                          'disabled' => $LJ::DISABLE_MEDIA_UPLOADS });
+                $body .= "<label for='rename_keyword_$pid'>$ML{'.label.rename'}</label>"; 
+                $body .= "</div>\n";
+            }
             $body .= "</div>\n";
 
             $body .= "<div class='userpic_comments pkg'>\n";
@@ -612,10 +628,15 @@ sub update_userpics
         if ($POST{"kw_$picid"} ne $POST{"kw_orig_$picid"}) {
             my $kws = $POST{"kw_$picid"};
             
-            eval {
-                $up->set_keywords($kws);
-            } or push @errors, $@;
-            
+            if ($POST{"rename_keyword_$picid"}) {
+                eval {
+                    $up->set_and_rename_keywords($kws, $POST{"kw_orig_$picid"});
+                } or push @errors, $@->as_html;
+            } else {
+                eval {
+                    $up->set_keywords($kws);
+                } or push @errors, $@;
+            }
         }
         
         eval {
diff -r e36a761d81b3 -r be246bd19221 htdocs/editpics.bml.text
--- a/htdocs/editpics.bml.text	Tue Nov 24 20:03:33 2009 +0000
+++ b/htdocs/editpics.bml.text	Tue Nov 24 20:12:09 2009 +0000
@@ -39,6 +39,14 @@
 .error.nourl=You must enter the URL of an image
 
 .error.nomediauploads.delete=Unable to delete userpics at this time.  Media modification is currently disabled for the site.
+
+.error.rename.blankkw=Error renaming '[[origkw]]' to '[[newkw]]':  renaming of blank (pic#) keywords not allowed.
+
+.error.rename.keywordexists=Cannot rename to '[[keyword]]':  keyword already exists.
+
+.error.rename.keywords=An error occurred trying to rename '[[origkw]]' to '[[newkw]]'; entries and comments with the old keywords may not have changed to use the new keywords.
+
+.error.rename.mismatchedlength=Error renaming '[[origkw]]' to '[[newkw]]':  must rename to the same number of keywords.
 
 .error.toolarge.separately=Image is too large; please submit it separately to resize.
 
@@ -89,6 +97,8 @@
 
 .label.keywords.desc=Comma-separated list of terms you'll use to select this picture.
 
+.label.rename=Rename Keywords
+
 .makedefault=Make this your <u>d</u>efault picture
 
 .makedefault.key|notes=Enter here a <b>lower-case</b> version of the letter you underlined in ".makedefault". This is the shortcut key for the makedefault option.
diff -r e36a761d81b3 -r be246bd19221 htdocs/stc/editpics.css
--- a/htdocs/stc/editpics.css	Tue Nov 24 20:03:33 2009 +0000
+++ b/htdocs/stc/editpics.css	Tue Nov 24 20:12:09 2009 +0000
@@ -88,6 +88,12 @@ hr {
 .userpic_defaultdelete {
     width: 200px;
 }
+.userpic_rename {
+    width: 200px;
+    margin-left: 6.5em;
+    margin-bottom: 4px;
+    margin-top: 4px;
+}
 * html .userpic_keywords,
 * html .userpic_comments,
 * html .userpic_descriptions {
--------------------------------------------------------------------------------