fu: Close-up of Fu, bringing a scoop of water to her mouth (Default)
fu ([personal profile] fu) wrote in [site community profile] changelog2010-10-01 05:27 pm

[dw-free] New dversion for userpics to make renaming userpics faster

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

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

Update userpic schema and add a migration tool to convert users from the old
to the new. We'll be rolling this out slowly and with a lot of checks to
make sure we get it right.

Patch by [personal profile] exor674.

Files modified:
  • bin/upgrading/d8d9-userpicrename.pl
  • bin/upgrading/proplists.dat
  • bin/upgrading/update-db-general.pl
  • cgi-bin/DW/External/XPostProtocol/LJXMLRPC.pm
  • cgi-bin/DW/User/DVersion/Migrate8To9.pm
  • cgi-bin/DW/Worker/ContentImporter/LiveJournal/Entries.pm
  • cgi-bin/DW/Worker/ContentImporter/Local/Comments.pm
  • cgi-bin/LJ/Comment.pm
  • cgi-bin/LJ/Constants.pm
  • cgi-bin/LJ/Entry.pm
  • cgi-bin/LJ/S2.pm
  • cgi-bin/LJ/S2/EntryPage.pm
  • cgi-bin/LJ/S2/ReplyPage.pm
  • cgi-bin/LJ/Talk.pm
  • cgi-bin/LJ/User.pm
  • cgi-bin/LJ/Userpic.pm
  • cgi-bin/ljlib.pl
  • cgi-bin/ljprotocol.pl
  • htdocs/admin/entryprops.bml
  • htdocs/talkpost.bml
  • htdocs/talkread.bml
--------------------------------------------------------------------------------
diff -r ebea5b13e8a4 -r f897918203ac bin/upgrading/d8d9-userpicrename.pl
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/upgrading/d8d9-userpicrename.pl	Sat Oct 02 01:27:29 2010 +0800
@@ -0,0 +1,148 @@
+#!/usr/bin/perl
+#
+# This code was forked from the LiveJournal project owned and operated
+# by Live Journal, Inc. The code has been modified and expanded by
+# Dreamwidth Studios, LLC. These files were originally licensed under
+# the terms of the license supplied by Live Journal, Inc, which can
+# currently be found at:
+#
+# http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt
+#
+# In accordance with the original license, this code and all its
+# modifications are provided under the GNU General Public License.
+# A copy of that license can be found in the LICENSE file included as
+# part of this distribution.
+#
+# Goes over every user, updating their dversion to 9 and
+# moves userpicmap2 over to userpicmap3
+#
+use strict;
+use warnings;
+BEGIN {
+    use lib "$ENV{'LJHOME'}/cgi-bin/";
+    require "ljlib.pl";
+}
+use Term::ReadLine;
+use Getopt::Long;
+use DW::User::DVersion::Migrate8To9;
+
+my $BLOCK_SIZE = 10_000; # get users in blocks of 10,000
+my $VERBOSE    = 0;      # print out extra info
+my $need_help;
+my @cluster;
+my @users;
+my $endtime;
+
+my $help = <<"END";
+Usage: $0 [options]
+Options:
+    --cluster=N Specify user cluster to work on (by default, all clusters)
+    --hours=N   Work no more than N hours (by default, work until all is done)
+    --user=N    Specify users to migrate (by default, all users on the specified clusters)
+    --verbose   Be noisy
+    --help      Print this help and exit
+END
+
+GetOptions(
+    "help"      => \$need_help,
+    "cluster=i" => \@cluster,
+    "user=s"    => \@users,
+    "verbose"   => \$VERBOSE,
+    "hours=i"   => sub { $endtime = $_[1]*3600+time(); },
+) or die $help;
+
+if ( $need_help ) {
+    print $help;
+    exit(0);
+}
+
+unless ( @cluster ) {
+    no warnings 'once';
+    @cluster = ( 0, @LJ::CLUSTERS );
+}
+
+my $dbh = LJ::get_db_writer()
+    or die "Could not connect to global master";
+
+my $users = join( ', ', map { $dbh->quote($_) } @users );
+
+my $term = new Term::ReadLine 'd8-d9 migrator';
+my $line = $term->readline( "Do you want to update to dversion 9 (userpicmap3)? [N/y] " );
+unless ( $line =~ /^y/i ) {
+    print "Not upgrading to dversion 9\n\n";
+    exit;
+}
+
+print "\n--- Upgrading users to dversion (userpicmap3) ---\n\n";
+
+# get user count
+my $total = $dbh->selectrow_array( "SELECT COUNT(*) FROM user WHERE dversion = 8" );
+print "\tTotal users at dversion 8: $total\n\n";
+
+my $migrated = 0;
+my $flag_stop_work = 0;
+
+MAIN_LOOP:
+foreach my $cid ( @cluster ) {
+
+    while ( 1 ) {
+        my $sth;
+        if ( @users ) {
+            $sth = $dbh->prepare( "SELECT userid FROM user WHERE dversion=8 AND clusterid=? AND user IN ($users) LIMIT $BLOCK_SIZE" );
+        } else {
+            $sth = $dbh->prepare( "SELECT userid FROM user WHERE dversion=8 AND clusterid=? LIMIT $BLOCK_SIZE" );
+        }
+        $sth->execute( $cid );
+        die $sth->errstr if $sth->err;
+
+        my $count = $sth->rows;
+        print "\tGot $count users on cluster $cid with dversion=8\n";
+        last unless $count;
+       
+        local( $SIG{TERM}, $SIG{INT}, $SIG{HUP} );
+        $SIG{TERM} = $SIG{INT} = $SIG{HUP} = sub { $flag_stop_work = 1; };
+        while ( my ( $userid ) = $sth->fetchrow_array ) {
+            if ( $flag_stop_work ) {
+                warn "Exiting by signal...";
+                last MAIN_LOOP;
+            }
+            if ( $endtime && time()>$endtime ) {
+                warn "Exiting by time condition...";
+                last MAIN_LOOP;
+            }
+
+            my $u = LJ::load_userid( $userid )
+                or die "Invalid userid: $userid";
+             
+            if ( $u->is_expunged ) {
+                ## special case: expunged (deleted) users
+                ## just update dbversion, don't move or delete(?) data
+                $u->update_self( { 'dversion' => 9 } );
+                print "\tUpgrading version of deleted user $u->{user}\n" if $VERBOSE;
+                $migrated++;
+            } else {
+                # lock while upgrading
+                my $lock = LJ::locker()->trylock( "d8d9-$userid" );
+                unless ( $lock ) {
+                    print STDERR "Could not get a lock for user " . $u->user . ".\n";
+                    next;
+                }
+
+                my $ok = eval { $u->upgrade_to_dversion_9 };
+                die $@ if $@;
+
+                print "\tMigrated user " . $u->user . "... " . ($ok ? 'ok' : 'ERROR') . "\n"
+                    if $VERBOSE;
+
+                $migrated++ if $ok;
+            }
+        }
+
+        print "\t - Migrated $migrated users so far\n\n";
+
+        # make sure we don't end up running forever for whatever reason
+        last if $migrated > $total;
+    }
+}
+
+print "--- Done migrating $migrated of $total users to dversion 9 ---\n";
diff -r ebea5b13e8a4 -r f897918203ac bin/upgrading/proplists.dat
--- a/bin/upgrading/proplists.dat	Sat Oct 02 00:29:19 2010 +0800
+++ b/bin/upgrading/proplists.dat	Sat Oct 02 01:27:29 2010 +0800
@@ -1161,6 +1161,11 @@ talkproplist.picture_keyword:
   des: A keyword that should align to a defined picture
   prettyname: Picture Keyword
 
+talkproplist.picture_mapid:
+  datatype: num
+  des: A keyword that should align to a mapid
+  prettyname: Picture Keyword MapID
+
 talkproplist.poster_ip:
   datatype: char
   des: The poster's IP address, optionally logged.
@@ -1324,6 +1329,13 @@ logproplist.picture_keyword:
   sortorder: 30
   ownership: user
 
+logproplist.picture_mapid:
+  datatype: num
+  des: A keyword that should align to a mapid
+  prettyname: Picture Keyword MapID
+  sortorder: 30
+  ownership: system
+
 logproplist.revnum:
   datatype: num
   des: Number of times this post has been edited.
diff -r ebea5b13e8a4 -r f897918203ac bin/upgrading/update-db-general.pl
--- a/bin/upgrading/update-db-general.pl	Sat Oct 02 00:29:19 2010 +0800
+++ b/bin/upgrading/update-db-general.pl	Sat Oct 02 01:27:29 2010 +0800
@@ -554,6 +554,20 @@ CREATE TABLE userpicmap2 (
     picid int(10) unsigned NOT NULL default '0',
 
     PRIMARY KEY  (userid, kwid)
+)
+EOC
+
+register_tablecreate("userpicmap3", <<'EOC');
+CREATE TABLE userpicmap3 (
+    userid int(10) unsigned NOT NULL default '0',
+    mapid int(10) unsigned NOT NULL,
+    kwid int(10) unsigned,
+    picid int(10) unsigned,
+    redirect_mapid int(10) unsigned,
+
+    PRIMARY KEY (userid, mapid),
+    UNIQUE KEY  (userid, kwid),
+    INDEX redirect (userid, redirect_mapid)
 )
 EOC
 
diff -r ebea5b13e8a4 -r f897918203ac cgi-bin/DW/External/XPostProtocol/LJXMLRPC.pm
--- a/cgi-bin/DW/External/XPostProtocol/LJXMLRPC.pm	Sat Oct 02 00:29:19 2010 +0800
+++ b/cgi-bin/DW/External/XPostProtocol/LJXMLRPC.pm	Sat Oct 02 01:27:29 2010 +0800
@@ -315,9 +315,12 @@ sub entry_to_req {
     my $entryprops = $entry->props;
     $req->{props} = {};
     # only bring over these properties
-    for my $entrykey (qw ( adult_content current_coords current_location current_music opt_backdated opt_nocomments opt_noemail opt_preformatted opt_screening picture_keyword taglist used_rte pingback )) {
+    for my $entrykey (qw ( adult_content current_coords current_location current_music opt_backdated opt_nocomments opt_noemail opt_preformatted opt_screening taglist used_rte pingback )) {
         $req->{props}->{$entrykey} = $entryprops->{$entrykey} if defined $entryprops->{$entrykey};
     }
+
+    # and regenerate this one from data
+    $req->{props}->{picture_keyword} = $entry->userpic_kw;
 
     # figure out what current_moodid and current_mood to pass to the crossposted entry
     my ( $siteid, $moodid, $mood ) = ( $extacct->siteid, $entryprops->{current_moodid}, $entryprops->{current_mood} );
diff -r ebea5b13e8a4 -r f897918203ac cgi-bin/DW/User/DVersion/Migrate8To9.pm
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cgi-bin/DW/User/DVersion/Migrate8To9.pm	Sat Oct 02 01:27:29 2010 +0800
@@ -0,0 +1,265 @@
+#!/usr/bin/perl
+#
+# DW::User::DVersion::Migrate8To9 - Handling dversion 8 to 9 migration
+#
+# Authors:
+#      Andrea Nall <anall@andreanall.com>
+#
+# Copyright (c) 2010 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'.
+
+package DW::User::DVersion::Migrate8To9;
+
+use strict;
+use warnings;
+use LJ::User;
+use Time::HiRes qw( usleep );
+
+my $readonly_bit;
+
+# find readonly cap class, complain if not found
+foreach (keys %LJ::CAP) {
+    if ($LJ::CAP{$_}->{'_name'} eq "_moveinprogress" &&
+        $LJ::CAP{$_}->{'readonly'} == 1) {
+        $readonly_bit = $_;
+        last;
+    }
+}
+unless (defined $readonly_bit) {
+    die "Won't move user without %LJ::CAP capability class named '_moveinprogress' with readonly => 1\n";
+}
+
+my $logpropid = LJ::get_prop( log => 'picture_keyword' )->{id};
+my $talkpropid = LJ::get_prop( talk => 'picture_keyword' )->{id};
+
+my $logpropid_map = LJ::get_prop( log => 'picture_mapid' )->{id};
+my $talkpropid_map = LJ::get_prop( talk => 'picture_mapid' )->{id};
+
+sub do_upgrade {
+    my $BLOCK_INSERT =   25;
+
+    my ( $u ) = @_;
+
+    return 0 if $u->readonly;
+
+    return 1 if $u->dversion >= 9;
+
+    # we really cannot have the user doing things during this process
+    $u->modify_caps( [$readonly_bit], [] );
+
+    # wait a quarter of a second, give any request the user might be doing a chance to stop
+    # as the user changing things could lead to slight data loss re. userpic selection
+    # on entries and comments
+    usleep( 250000 );
+
+    # do this in an eval so, in case something dies, we don't leave the user locked
+    my $rv = 0;
+
+    eval {
+        # Unfortunately, we need to iterate over all clusters to get a list
+        # of used keywords so we can give proper ids to everything,
+        # even removed keywords
+        my %keywords;
+        my %to_update;
+        if ( $u->is_individual ) {
+            foreach my $cluster_id ( @LJ::CLUSTERS ) {
+                my $dbcm_o = LJ::get_cluster_master( $cluster_id );
+
+                my $entries = $dbcm_o->selectall_arrayref(q{
+                    SELECT log2.journalid AS journalid, 
+                           log2.jitemid AS jitemid,
+                           logprop2.value AS value
+                    FROM logprop2 
+                    INNER JOIN log2 
+                        ON ( logprop2.journalid = log2.journalid 
+                            AND logprop2.jitemid = log2.jitemid ) 
+                    WHERE posterid = ? 
+                        AND propid=?
+                    }, undef, $u->id, $logpropid);
+                die $dbcm_o->errstr if $dbcm_o->err;
+                my $comments = $dbcm_o->selectall_arrayref( q{
+                    SELECT talkprop2.journalid AS journalid,
+                           talkprop2.jtalkid AS jtalkid,
+                           talkprop2.value AS value
+                    FROM talkprop2
+                    INNER JOIN talk2
+                        ON ( talkprop2.journalid = talk2.journalid
+                            AND talkprop2.jtalkid = talk2.jtalkid )
+                    WHERE posterid = ?
+                        AND tpropid=?
+                    }, undef, $u->id, $talkpropid);
+                die $dbcm_o->errstr if $dbcm_o->err;
+
+                $to_update{$cluster_id} = {
+                    entries => $entries,
+                    comments => $comments,
+                };
+                $keywords{$_->[2]}->{count}++ foreach ( @$entries, @$comments );
+            }
+        }
+
+        my $origmap = $u->selectall_hashref( q{
+            SELECT kwid, picid FROM userpicmap2 WHERE userid=?
+            }, "kwid", undef, $u->id);
+        die $u->errstr if $u->err;
+
+        my $picmap = $u->selectall_hashref( q{
+            SELECT picid, state FROM userpic2 WHERE userid=?
+            }, "picid", undef, $u->id);
+        die $u->errstr if $u->err;
+
+        my %outrows;
+
+        my %kwid_map;
+
+        foreach my $k ( keys %keywords ) {
+            if ( $k =~ m/^pic#(\d+)$/ ) {
+                my $picid = $1;
+                next if ! exists $picmap->{$picid} || $picmap->{$picid}->{state} eq 'X';
+                $keywords{$k}->{kwid} = undef;
+                $keywords{$k}->{picid} = $picid;
+                $outrows{$picid}->{0}++;
+            } else {
+                my $kwid = $u->get_keyword_id($k,1);
+                $kwid_map{$kwid} = $k;
+                my $picid = $origmap->{$kwid}->{picid};
+                $keywords{$k}->{kwid} = $kwid;
+                $keywords{$k}->{picid} = $picid;
+                $outrows{$picid || 0}->{$kwid}++;
+            }
+        }
+
+        foreach my $r ( values %$origmap ) {
+            $outrows{$r->{picid}}->{$r->{kwid}}++ if $r->{picid} && $r->{kwid};
+        }
+
+        {
+            my ( @bind, @vals );
+
+            # flush rows to destination table
+            my $flush = sub {
+                return unless @bind;
+
+                # insert data
+                my $bind = join( ",", @bind );
+                $u->do( "REPLACE INTO userpicmap3 (userid,mapid,kwid,picid) VALUES $bind", undef, @vals );
+                die $u->errstr if $u->err;
+
+                # reset values
+                @bind = ();
+                @vals = ();
+            };
+
+            foreach my $picid ( sort { $a <=> $b } keys %outrows ) {
+                foreach my $kwid ( sort { $a <=> $b } keys %{$outrows{$picid}} ) {
+                    next if $kwid == 0 && $picid == 0;
+                    push @bind, "(?,?,?,?)";
+                    my $mapid = LJ::alloc_user_counter( $u, 'Y' );
+                    my $keyword = $kwid == 0 ? "pic#$picid" : $kwid_map{$kwid};
+                    # if $keyword is undef, this isn't used on any entries, so we don't care about the mapid
+                    # however, if $kwid is undef, this is a pic#xxx keyword, and had to have existed on an entry
+                    $keywords{$keyword}->{mapid} = $mapid if defined $keyword;
+                    push @vals, ( $u->id, $mapid, $kwid || undef, $picid || undef );
+                    $flush->() if @bind > $BLOCK_INSERT;
+                }
+            }
+            $flush->();
+        }
+
+        if ( $u->is_individual ) {
+            foreach my $cluster_id ( @LJ::CLUSTERS ) {
+                next unless $to_update{$cluster_id};
+                my $data = $to_update{$cluster_id};
+
+                my $dbcm_o = LJ::get_cluster_master($cluster_id);
+
+                {
+                    my ( @bind, @vals );
+
+                    # flush rows to destination table
+                    my $flush = sub {
+                        return unless @bind;
+
+                        # insert data
+                        my $bind = join( ",", @bind );
+                        $dbcm_o->do( "REPLACE INTO logprop2 (journalid,jitemid,propid,value) VALUES $bind", undef, @vals );
+                        die $u->errstr if $u->err;
+
+                        # reset values
+                        @bind = ();
+                        @vals = ();
+                    };
+
+                    foreach my $entry ( @{ $data->{entries} } ) {
+                        next unless $keywords{$entry->[2]}->{mapid};
+                        push @bind, "(?,?,?,?)";
+                        push @vals, ( $entry->[0], $entry->[1], $logpropid_map, $keywords{$entry->[2]}->{mapid} );
+                        $flush->() if @bind > $BLOCK_INSERT;
+                    }
+                    $flush->();
+
+                    foreach my $entry ( @{ $data->{entries} } ) {
+                        LJ::MemCache::delete([$entry->[0],"logprop:". $entry->[0] .":". $entry->[1]]);
+                    }
+                }
+                {
+                    my ( @bind, @vals );
+
+                    # flush rows to destination table
+                    my $flush = sub {
+                        return unless @bind;
+
+                        # insert data
+                        my $bind = join( ",", @bind );
+                        $dbcm_o->do( "REPLACE INTO talkprop2 (journalid,jtalkid,tpropid,value) VALUES $bind", undef, @vals );
+                        die $u->errstr if $u->err;
+
+                        # reset values
+                        @bind = ();
+                        @vals = ();
+                    };
+
+                    foreach my $comment ( @{ $data->{comments} } ) {
+                        next unless $keywords{$comment->[2]}->{mapid};
+                        push @bind, "(?,?,?,?)";
+                        push @vals, ( $comment->[0], $comment->[1], $talkpropid_map, $keywords{$comment->[2]}->{mapid} );
+                        $flush->() if @bind > $BLOCK_INSERT;
+                    }
+                    $flush->();
+
+                    foreach my $comment ( @{ $data->{comments} } ) {
+                        LJ::MemCache::delete([$comment->[0],"talkprop:". $comment->[0] .":". $comment->[1]]);
+                    }
+                }
+            }
+        }
+
+        $rv = 1;
+    };
+
+    my $err = $@;
+
+    # okay, we're done, the user can do things again
+    $u->modify_caps( [], [$readonly_bit] );
+
+    die $err if $err;
+
+    return $rv;
+}
+
+sub upgrade_to_dversion_9 {
+    # If user has been purged, go ahead and update version
+    # Otherwise move their data
+    my $ok = $_[0]->is_expunged ? 1 : do_upgrade(@_);
+
+    $_[0]->update_self( { 'dversion' => 9 } ) if $ok;
+
+    LJ::Userpic->delete_cache( $_[0] );
+
+    return $ok;
+}
+
+*LJ::User::upgrade_to_dversion_9 = \&upgrade_to_dversion_9;
diff -r ebea5b13e8a4 -r f897918203ac cgi-bin/DW/Worker/ContentImporter/LiveJournal/Entries.pm
--- a/cgi-bin/DW/Worker/ContentImporter/LiveJournal/Entries.pm	Sat Oct 02 00:29:19 2010 +0800
+++ b/cgi-bin/DW/Worker/ContentImporter/LiveJournal/Entries.pm	Sat Oct 02 01:27:29 2010 +0800
@@ -207,7 +207,12 @@ sub try_work {
             # local picture keyword
             if ( my $jitemid = $entry_map->{$evt->{key}} ) {
                 my $entry = LJ::Entry->new( $u, jitemid => $jitemid );
-                $entry->set_prop( picture_keyword => $evt->{props}->{picture_keyword} );
+                my $kw = $evt->{props}->{picture_keyword};
+                if ( $u->userpic_have_mapid ) {
+                    $entry->set_prop( picture_mapid => $u->get_mapid_from_keyword( $kw, create => 1) );
+                } else {
+                    $entry->set_prop( picture_keyword => $kw );
+                }
             }
 
             # now try to skip it
diff -r ebea5b13e8a4 -r f897918203ac cgi-bin/DW/Worker/ContentImporter/Local/Comments.pm
--- a/cgi-bin/DW/Worker/ContentImporter/Local/Comments.pm	Sat Oct 02 00:29:19 2010 +0800
+++ b/cgi-bin/DW/Worker/ContentImporter/Local/Comments.pm	Sat Oct 02 01:27:29 2010 +0800
@@ -71,7 +71,12 @@ sub update_comment {
     # edits and such.  for now, I'm just trying to get the icons to update...
     my $c = LJ::Comment->instance( $u, jtalkid => $cmt->{id} )
         or return $$errref = 'Unable to instantiate LJ::Comment object.';
-    $c->set_prop( picture_keyword => $cmt->{props}->{picture_keyword} );
+    my $pu = $c->poster;
+    if ( $pu && $pu->userpic_have_mapid ) {
+        $c->set_prop( picture_mapid => $u->get_mapid_from_keyword( $cmt->{props}->{picture_keyword}, create => 1 ) );
+    } else {
+        $c->set_prop( picture_keyword => $cmt->{props}->{picture_keyword} );
+    }
 }
 
 
diff -r ebea5b13e8a4 -r f897918203ac cgi-bin/LJ/Comment.pm
--- a/cgi-bin/LJ/Comment.pm	Sat Oct 02 00:29:19 2010 +0800
+++ b/cgi-bin/LJ/Comment.pm	Sat Oct 02 01:27:29 2010 +0800
@@ -26,6 +26,14 @@ use lib "$LJ::HOME/cgi-bin";
 
 require "htmlcontrols.pl";
 use LJ::Talk;
+
+=head1 NAME
+
+LJ::Comment
+
+=head1 CLASS METHODS
+
+=cut
 
 # internal fields:
 #
@@ -218,6 +226,9 @@ sub create {
 
 }
 
+=head1 INSTANCE METHODS
+
+=cut
 
 sub absorb_row {
     my ($self, %row) = @_;
@@ -303,24 +314,6 @@ sub edit_url {
     my $url     = $entry->url;
 
     return "$url?edit=$dtalkid";
-}
-
-# return img tag of userpic that the comment poster used
-sub poster_userpic {
-    my $self = $_[0];
-    my $pic_kw = $self->prop('picture_keyword');
-    my $posteru = $self->poster;
-
-    # anonymous poster, no userpic
-    return "" unless $posteru;
-
-    # new from keyword falls back to the default userpic if
-    # there was no keyword, or if the keyword is no longer used
-    my $pic = LJ::Userpic->new_from_keyword($posteru, $pic_kw);
-    return $pic->imgtag_nosize if $pic;
-
-    # no userpic with comment
-    return "";
 }
 
 # return LJ::User of journal comment is in
@@ -1383,10 +1376,8 @@ sub _format_mail_both {
 
     if ($is_html) {
         my $pichtml;
-        my $pic_kw = $self->prop('picture_keyword');
-
         if ( $posteru ) {
-            my $pic = LJ::Userpic->new_from_keyword( $posteru, $pic_kw ) || $posteru->userpic;
+            my ( $pic, $pic_kw ) = $self->userpic;
 
             if ( $pic && $pic->load_row ) {
                 $pichtml = "<img src=\"$LJ::USERPIC_ROOT/$pic->{picid}/$pic->{userid}\" align='absmiddle' ".
@@ -1553,20 +1544,45 @@ sub is_text_spam($\$) {
     return 0; # normal text
 }
 
-# returns a LJ::Userpic object for the poster of the comment, or undef
-# it will unify interface between Entry and Comment: $foo->userpic will
-# work correctly for both Entry and Comment objects
+=head2 C<< $cmt->userpic >>
+
+Returns a LJ::Userpic object for the poster of the comment, or undef.
+
+If called in a list context, returns ( LJ::Userpic object, keyword )
+
+=cut
 sub userpic {
     my $self = $_[0];
 
     my $up = $self->poster;
     return unless $up;
 
-    my $key = $self->prop('picture_keyword');
-
     # return the picture from keyword, if defined
     # else return poster's default userpic
-    return LJ::Userpic->new_from_keyword( $up, $key ) || $up->userpic;
+    my $kw = $_[0]->userpic_kw;
+    my $pic = LJ::Userpic->new_from_keyword( $up, $kw ) || $up->userpic;
+
+    return wantarray ? ( $pic, $kw ) : $pic;
+}
+
+=head2 C<< $cmt->userpic_kw >>
+
+Returns the userpic keyword used on this comment, or undef.
+
+=cut
+sub userpic_kw {
+    my $self = $_[0];
+
+    my $up = $self->poster;
+    return unless $up;
+
+    if ( $up->userpic_have_mapid ) {
+        my $mapid = $self->prop('picture_mapid');
+
+        return $up->get_keyword_from_mapid( $mapid ) if $mapid;
+    } else {
+        return $self->prop('picture_keyword');
+    }
 }
 
 sub poster_ip {
diff -r ebea5b13e8a4 -r f897918203ac cgi-bin/LJ/Constants.pm
--- a/cgi-bin/LJ/Constants.pm	Sat Oct 02 00:29:19 2010 +0800
+++ b/cgi-bin/LJ/Constants.pm	Sat Oct 02 01:27:29 2010 +0800
@@ -68,11 +68,12 @@ use constant CMAX_UPIC_DESCRIPTION => 12
 #    6: clustered memories, friend groups, and keywords (for memories)
 #    7: clustered userpics, keyword limiting, and comment support
 #    8: clustered polls
+#    9: userpicmap3, with mapid
 #
 # Dreamwidth installations should ALL be dversion >= 8.  We do not support anything
 # else and are ripping out code to support all previous dversions.
 #
-use constant MAX_DVERSION => 8;
+use constant MAX_DVERSION => 9;
 $LJ::MAX_DVERSION = MAX_DVERSION;
 
 1;
diff -r ebea5b13e8a4 -r f897918203ac cgi-bin/LJ/Entry.pm
--- a/cgi-bin/LJ/Entry.pm	Sat Oct 02 00:29:19 2010 +0800
+++ b/cgi-bin/LJ/Entry.pm	Sat Oct 02 01:27:29 2010 +0800
@@ -21,6 +21,14 @@ use strict;
 use strict;
 use vars qw/ $AUTOLOAD /;
 use Carp qw/ croak confess /;
+
+=head1 NAME
+
+LJ::Entry
+
+=head1 CLASS METHODS
+
+=cut
 
 # internal fields:
 #
@@ -186,6 +194,10 @@ sub new_from_row {
 
     return $self;
 }
+
+=head1 INSTANCE METHODS
+
+=cut
 
 # returns true if entry currently exists.  (it's possible for a given
 # $u, to make a fake jitemid and that'd be a valid skeleton LJ::Entry
@@ -854,35 +866,54 @@ sub tag_map {
     return $tags->{$self->jitemid} || {};
 }
 
-# returns a LJ::Userpic object for this post, or undef
-# currently this is for the permalink view, not for the friends view
-# context.  TODO: add a context option for friends page, and perhaps
+=head2 C<< $entry->userpic >>
+
+Returns a LJ::Userpic object for this post, or undef.
+
+If called in a list context, returns ( LJ::Userpic object, keyword )
+
+See userpic_kw.
+
+=cut
+# FIXME: add a context option for friends page, and perhaps
 # respect $remote's userpic viewing preferences (community shows poster
 # vs community's picture)
 sub userpic {
-    my $self = shift;
+    my $up = $_[0]->poster;
+    my $kw = $_[0]->userpic_kw;
+    my $pic = LJ::Userpic->new_from_keyword( $up, $kw ) || $up->userpic;
+
+    return wantarray ? ( $pic, $kw ) : $pic;
+}
+
+=head2 C<< $entry->userpic_kw >>
+
+Returns the keyword to use for the entry.
+
+If a keyword is specified, it uses that, otherwise
+it tries the custom mood text, followed by the standard mood.
+
+=cut
+sub userpic_kw {
+    my $self = $_[0];
 
     my $up = $self->poster;
+    
+    my $key;
+    # try their entry-defined userpic keyword
+    if ( $up->userpic_have_mapid ) {
+        my $mapid = $self->prop('picture_mapid');
 
-    # try their entry-defined userpic keyword, then their custom
+        $key = $up->get_keyword_from_mapid( $mapid ) if $mapid;
+    } else {
+        $key = $self->prop('picture_keyword');
+    }
+
+    # ... but if that fails, then their custom
     # mood, then their standard mood
-    my $key = $self->prop('picture_keyword') ||
-        $self->prop('current_mood') ||
+    return $key || $self->prop('current_mood') ||
         DW::Mood->mood_name( $self->prop('current_moodid') );
-
-    # return the picture from keyword, if defined
-    # else return poster's default userpic
-    return LJ::Userpic->new_from_keyword( $up, $key ) || $up->userpic;
 }
-
-sub userpic_kw_from_props {
-    my ($class, $props) = @_;
-
-    return $props->{'picture_keyword'} ||
-        $props->{'current_mood'} ||
-        DW::Mood->mood_name( $props->{'current_moodid'} );
-}
-
 
 # returns true if the user is allowed to share an entry via Tell a Friend
 # $u is the logged-in user
diff -r ebea5b13e8a4 -r f897918203ac cgi-bin/LJ/S2.pm
--- a/cgi-bin/LJ/S2.pm	Sat Oct 02 00:29:19 2010 +0800
+++ b/cgi-bin/LJ/S2.pm	Sat Oct 02 01:27:29 2010 +0800
@@ -1960,11 +1960,11 @@ sub Entry_from_entryobj
 
     # loading S2 Userpic
     my $userpic;
-    my $kw = $entry_obj->userpic_kw_from_props( $entry_obj->props );
+    my ( $pic, $kw ) = $entry_obj->userpic;
 
     # if the post was made in a community, use either the userpic it was posted with or the community pic depending on the style setting
     if ( $posterid == $journalid || !S2::get_property_value($opts->{ctx}, 'use_shared_pic') ) {
-        $userpic = Image_userpic( $poster, $entry_obj->userpic->picid, $kw ) if $entry_obj->userpic;
+        $userpic = Image_userpic( $poster, $pic->picid, $kw ) if $pic;
     } else {
         $userpic = Image_userpic( $journal, $journal->userpic->picid ) if $journal->userpic;
     }
diff -r ebea5b13e8a4 -r f897918203ac cgi-bin/LJ/S2/EntryPage.pm
--- a/cgi-bin/LJ/S2/EntryPage.pm	Sat Oct 02 00:29:19 2010 +0800
+++ b/cgi-bin/LJ/S2/EntryPage.pm	Sat Oct 02 01:27:29 2010 +0800
@@ -181,7 +181,7 @@ sub EntryPage
                     $height = $height / 2;
                 }
 
-                $comment_userpic = Image_userpic( $com->{upost}, $com->{picid}, $com->{props}->{picture_keyword}, 
+                $comment_userpic = Image_userpic( $com->{upost}, $com->{picid}, $com->{pickw}, 
                                                   $width, $height );
             }
 
@@ -226,7 +226,7 @@ sub EntryPage
                 '_type' => 'Comment',
                 'journal' => $userlite_journal,
                 'metadata' => {
-                    'picture_keyword' => $com->{'props'}->{'picture_keyword'},
+                    'picture_keyword' => $com->{pickw},
                 },
                 'permalink_url' => "$permalink?thread=$dtalkid" . LJ::Talk::comment_anchor( $dtalkid ),
                 'reply_url' => $reply_url,
@@ -496,8 +496,8 @@ sub EntryPage_entry
     
     # load the userpic; include the keyword selected by the user
     # as a backup for the alttext
-    my $pickw = LJ::Entry->userpic_kw_from_props($entry->props);
-    my $userpic = Image_userpic($pu, $entry->userpic ? $entry->userpic->picid : 0, $pickw);
+    my ( $pic, $pickw ) = $entry->userpic;
+    my $userpic = Image_userpic($pu, $pic ? $pic->picid : 0, $pickw);
 
     my $comments = CommentInfo( $entry->comment_info(
         u => $u, remote => $remote, style_args => $style_args, viewall => $viewall
diff -r ebea5b13e8a4 -r f897918203ac cgi-bin/LJ/S2/ReplyPage.pm
--- a/cgi-bin/LJ/S2/ReplyPage.pm	Sat Oct 02 00:29:19 2010 +0800
+++ b/cgi-bin/LJ/S2/ReplyPage.pm	Sat Oct 02 01:27:29 2010 +0800
@@ -118,8 +118,8 @@ sub ReplyPage
         $comment_values{subject} = $comment->subject_orig;
         $comment_values{body} = $comment->body_orig;
         $comment_values{subjecticon} = $comment->prop('subjecticon');
-        $comment_values{prop_picture_keyword} = $comment->prop('picture_keyword');
         $comment_values{prop_opt_preformatted} = $comment->prop('opt_preformatted');
+        $comment_values{prop_picture_keyword} = $comment->userpic_kw;
     }
 
     if ($replytoid) {
@@ -130,6 +130,8 @@ sub ReplyPage
             return;
         }
 
+        # FIXME: Why are we loading the comment manually when we do LJ::Comment->new below
+        # and could do everything through there.
         my $sql = "SELECT jtalkid, posterid, state, datepost FROM talk2 ".
             "WHERE journalid=$u->{'userid'} AND jtalkid=$re_talkid ".
             "AND nodetype='L' AND nodeid=" . $entry->jitemid;
@@ -177,16 +179,17 @@ sub ReplyPage
         }
 
         my $datetime = DateTime_unix(LJ::mysqldate_to_time($parpost->{'datepost'}));
-
-        my ($s2poster, $pu);
+        
         my $comment_userpic;
-        if ($parpost->{'posterid'}) {
-            $pu = LJ::load_userid($parpost->{'posterid'});
+        my $s2poster;
+        
+        my $pu = $parentcomment->poster;
+        if ( $pu ) {
             return $opts->{handler_return} = 403 if $pu->is_suspended; # do not show comments by suspended users
             $s2poster = UserLite($pu);
 
-            my $pickw = LJ::Entry->userpic_kw_from_props($parpost->{'props'});
-            $comment_userpic = Image_userpic($pu, 0, $pickw);
+            my ( $pic, $pickw ) = $parentcomment->userpic;
+            $comment_userpic = Image_userpic($pu, $pic ? $pic->picid : 0, $pickw);
         }
 
         LJ::CleanHTML::clean_comment(\$parpost->{'body'},
diff -r ebea5b13e8a4 -r f897918203ac cgi-bin/LJ/Talk.pm
--- a/cgi-bin/LJ/Talk.pm	Sat Oct 02 00:29:19 2010 +0800
+++ b/cgi-bin/LJ/Talk.pm	Sat Oct 02 01:27:29 2010 +0800
@@ -1147,28 +1147,38 @@ sub load_comments
     }
 
     # optionally give them back user refs
-    if (ref($opts->{'userref'}) eq "HASH") {
+    if (ref($opts->{userref}) eq "HASH") {
         my %userpics = ();
         # copy into their ref the users we've already loaded above.
         while (my ($k, $v) = each %up) {
-            $opts->{'userref'}->{$k} = $v;
+            $opts->{userref}->{$k} = $v;
         }
 
         # optionally load userpics
-        if (ref($opts->{'userpicref'}) eq "HASH") {
+        if (ref($opts->{userpicref}) eq "HASH") {
             my @load_pic;
             foreach my $talkid (@posts_to_load) {
                 my $post = $posts->{$talkid};
-                my $kw;
-                if ($post->{'props'} && $post->{'props'}->{'picture_keyword'}) {
-                    $kw = $post->{'props'}->{'picture_keyword'};
+                my $pu = $opts->{userref}->{$post->{posterid}};
+                my ( $id, $kw );
+                if ( $pu && $pu->userpic_have_mapid ) {
+                    my $mapid;
+                    if ($post->{props} && $post->{props}->{picture_mapid}) {
+                        $mapid = $post->{props}->{picture_mapid};
+                    }
+                    $kw = $pu ? $pu->get_keyword_from_mapid( $mapid ) : undef;
+                    $id = $pu ? $pu->get_picid_from_mapid( $mapid ) : undef;
+                } else {
+                    if ($post->{props} && $post->{props}->{picture_keyword}) {
+                        $kw = $post->{props}->{picture_keyword};
+                    }
+                    $id = $pu ? $pu->get_picid_from_keyword( $kw ) : undef;
                 }
-                my $pu = $opts->{'userref'}->{$post->{'posterid'}};
-                my $id = $pu ? $pu->get_picid_from_keyword( $kw ) : undef;
-                $post->{'picid'} = $id;
+                $post->{picid} = $id;
+                $post->{pickw} = $kw;
                 push @load_pic, [ $pu, $id ];
             }
-            load_userpics( $opts->{'userpicref'}, \@load_pic );
+            load_userpics( $opts->{userpicref}, \@load_pic );
         }
     }
     return map { $posts->{$_} } @top_replies;
@@ -2855,7 +2865,12 @@ sub enter_comment {
     $talkprop{'unknown8bit'} = 1 if $comment->{unknown8bit};
     $talkprop{'subjecticon'} = $comment->{subjecticon};
 
-    $talkprop{'picture_keyword'} = $comment->{picture_keyword};
+    my $pu = $comment->{u};
+    if ( $pu && $pu->userpic_have_mapid ) {
+        $talkprop{picture_mapid} = $pu->get_mapid_from_keyword( $comment->{picture_keyword} );
+    } else {
+        $talkprop{picture_keyword} = $comment->{picture_keyword};
+    }
 
     $talkprop{'opt_preformatted'} = $comment->{preformat} ? 1 : 0;
     if ($journalu->opt_logcommentips eq "A" ||
@@ -3015,7 +3030,14 @@ sub enter_imported_comment {
 
     $talkprop{'unknown8bit'}      = 1 if $comment->{unknown8bit};
     $talkprop{'subjecticon'}      = $comment->{subjecticon};
-    $talkprop{'picture_keyword'}  = $comment->{picture_keyword};
+
+    my $pu = $comment->{u};
+    if ( $pu && $pu->userpic_have_mapid ) {
+        $talkprop{picture_mapid} = $pu->get_mapid_from_keyword( $comment->{picture_keyword}, create => 1 );
+    } else {
+        $talkprop{picture_keyword} = $comment->{picture_keyword};
+    }
+
     $talkprop{'opt_preformatted'} = $comment->{preformat} ? 1 : 0;
 
     # remove blank/0 values (defaults)
@@ -3140,7 +3162,7 @@ sub init {
         $form->{'userpost'} = $remote->{'user'};
         $form->{'usertype'} = "user";
     }
-    # XXXevan hack:  remove me when we fix preview.
+    # FIXME: XXXevan hack:  remove me when we fix preview.
     $init->{cookie_auth} = $cookie_auth;
 
     # test accounts may only comment on other test accounts.
@@ -3665,10 +3687,16 @@ sub edit_comment {
 
     my %props = (
         subjecticon => $comment->{subjecticon},
-        picture_keyword => $comment->{picture_keyword},
         opt_preformatted => $comment->{preformat} ? 1 : 0,
         edit_reason => $comment->{editreason},
     );
+
+    my $pu = $comment_obj->poster;
+    if ( $pu && $pu->userpic_have_mapid ) {
+        $props{picture_mapid} = $pu->get_mapid_from_keyword( $comment->{picture_keyword} );
+    } else {
+        $props{picture_keyword} = $comment->{picture_keyword};
+    }
 
     # set most of the props together
     $comment_obj->set_props(%props);
@@ -3689,7 +3717,7 @@ sub edit_comment {
     $comment->{talkid} = $comment_obj->jtalkid;
 
     # cluster tracking
-    LJ::mark_user_active($comment_obj->poster, 'comment');
+    LJ::mark_user_active($pu, 'comment');
 
     # fire events
     if ( LJ::is_enabled('esn') ) {
diff -r ebea5b13e8a4 -r f897918203ac cgi-bin/LJ/User.pm
--- a/cgi-bin/LJ/User.pm	Sat Oct 02 00:29:19 2010 +0800
+++ b/cgi-bin/LJ/User.pm	Sat Oct 02 01:27:29 2010 +0800
@@ -6082,6 +6082,7 @@ sub activate_userpics {
     return 1 if $u->is_expunged;
 
     my $userid = $u->userid;
+    my $have_mapid = $u->userpic_have_mapid;
 
     # active / inactive lists
     my @active = ();
@@ -6117,33 +6118,45 @@ sub activate_userpics {
 
         # query all pickws in logprop2 with jitemid > that value
         my %count_kw = ();
-        my $propid = LJ::get_prop("log", "picture_keyword")->{'id'};
+        my $propid;
+        if ( $have_mapid ) {
+            $propid = LJ::get_prop("log", "picture_mapid")->{id};
+        } else {
+            $propid = LJ::get_prop("log", "picture_keyword")->{id};
+        }
         my $sth = $dbcr->prepare("SELECT value, COUNT(*) FROM logprop2 " .
                                  "WHERE journalid=? AND jitemid > ? AND propid=?" .
                                  "GROUP BY value");
-        $sth->execute($userid, $jitemid, $propid);
+        $sth->execute($userid, $jitemid || 0, $propid);
         while (my ($value, $ct) = $sth->fetchrow_array) {
             # keyword => count
             $count_kw{$value} = $ct;
         }
 
-        my $keywords_in = join(",", map { $dbh->quote($_) } keys %count_kw);
+        my $values_in = join(",", map { $dbh->quote($_) } keys %count_kw);
 
         # map pickws to picids for freq hash below
         my %count_picid = ();
-        if ($keywords_in) {
-            my $sth = $dbcr->prepare( "SELECT k.keyword, m.picid FROM userkeywords k, userpicmap2 m ".
-                                      "WHERE k.keyword IN ($keywords_in) AND k.kwid=m.kwid AND k.userid=m.userid " .
-                                      "AND k.userid=?" );
-            $sth->execute($userid);
-            while (my ($keyword, $picid) = $sth->fetchrow_array) {
-                # keyword => picid
-                $count_picid{$picid} += $count_kw{$keyword};
+        if ( $values_in ) {
+            if ( $have_mapid ) {
+                foreach my $mapid ( keys %count_kw ) {
+                    my $picid = $u->get_picid_from_mapid($mapid);
+                    $count_picid{$picid} += $count_kw{$mapid} if $picid;
+                }
+            } else {
+                my $sth = $dbcr->prepare( "SELECT k.keyword, m.picid FROM userkeywords k, userpicmap2 m ".
+                                        "WHERE k.keyword IN ($values_in) AND k.kwid=m.kwid AND k.userid=m.userid " .
+                                        "AND k.userid=?" );
+                $sth->execute($userid);
+                while (my ($keyword, $picid) = $sth->fetchrow_array) {
+                    # keyword => picid
+                    $count_picid{$picid} += $count_kw{$keyword};
+                }
             }
         }
 
         # we're only going to ban the least used, excluding the user's default
-        my @ban = (grep { $_ != $u->{'defaultpicid'} }
+        my @ban = (grep { $_ != $u->{defaultpicid} }
                    sort { $count_picid{$a} <=> $count_picid{$b} } @active);
 
         @ban = splice(@ban, 0, $to_ban) if @ban > $to_ban;
@@ -6171,6 +6184,7 @@ sub activate_userpics {
 
     # delete userpic info object from memcache
     LJ::Userpic->delete_cache($u);
+    $u->clear_userpic_kw_map;
 
     return 1;
 }
@@ -6221,7 +6235,13 @@ sub expunge_userpic {
     # else now mark it
     $u->do( "UPDATE userpic2 SET state='X' WHERE userid = ? AND picid = ?", undef, $u->userid, $picid );
     return LJ::error( $dbcm ) if $dbcm->err;
+    
+    # Since we don't clean userpicmap2 when we migrate to dversion 9, clean it here on expunge no matter the dversion.
     $u->do( "DELETE FROM userpicmap2 WHERE userid = ? AND picid = ?", undef, $u->userid, $picid );
+    if ( $u->userpic_have_mapid ) {
+        $u->do( "DELETE FROM userpicmap3 WHERE userid = ? AND picid = ? AND kwid=NULL", undef, $u->userid, $picid );
+        $u->do( "UPDATE userpicmap3 SET picid = NULL WHERE userid = ? AND picid = ?", undef, $u->userid, $picid );
+    }
 
     # now clear the user's memcache picture info
     LJ::Userpic->delete_cache( $u );
@@ -6231,6 +6251,111 @@ sub expunge_userpic {
     return ( $u->userid, map {$_->[0]} grep {$_ && @$_ && $_->[0]} @rval );
 }
 
+=head3 C<< $u->get_keyword_from_mapid( $mapid, %opts ) >>
+
+Returns the keyword for the given mapid or undef if the mapid doesn't exist.
+
+Arguments:
+
+=over 4
+
+=item mapid
+
+=back
+
+Additional options:
+
+=over 4
+
+=item redir_callback
+
+Called if the mapping is redirected to another mapping with the following arguments
+
+( $u, $old_mapid, $new_mapid )
+
+=back
+
+=cut
+sub get_keyword_from_mapid {
+    my ( $u, $mapid, %opts ) = @_;
+    my $info = LJ::isu( $u ) ? $u->get_userpic_info : undef;
+    return undef unless $info;
+    return undef unless $u->userpic_have_mapid;
+
+    $mapid = $u->resolve_mapid_redirects($mapid,%opts);
+    my $kw = $info->{mapkw}->{ $mapid };
+    return $kw;
+}
+
+=head3 C<< $u->get_mapid_from_keyword( $kw, %opts ) >>
+
+Returns the mapid for a given keyword.
+
+Arguments:
+
+=over 4
+
+=item kw
+
+The keyword.
+
+=back
+
+Additional options:
+
+=over 4
+
+=item create
+
+Should a mapid be created if one does not exist.
+
+Default: 0
+
+=back
+
+=cut
+sub get_mapid_from_keyword {
+    my ( $u, $kw, %opts ) = @_;
+    return 0 unless $u->userpic_have_mapid;
+
+    my $info = LJ::isu( $u ) ? $u->get_userpic_info : undef;
+    return 0 unless $info;
+
+    my $mapid = $info->{kwmap}->{$kw};
+    return $mapid if $mapid;
+
+    # the silly "pic#2343" thing when they didn't assign a keyword, if we get here
+    # we need to create it.
+    if ( $kw =~ /^pic\#(\d+)$/ ) {
+        my $picid = $1;
+        return 0 unless $info->{pic}{$picid};           # don't create rows for invalid pics
+        return 0 unless $info->{pic}{$picid}{state} eq 'N'; # or inactive
+
+        return $u->_create_mapid( undef, $picid )
+    }
+
+    return 0 unless $opts{create};
+
+    return $u->_create_mapid( $u->get_keyword_id( $kw ), undef );
+}
+
+=head3 C<< $u->get_picid_from_keyword( $kw, $default ) >>
+
+Returns the picid for a given keyword.
+
+=over 4
+
+=item kw
+
+Keyword to look up.
+
+=item default (optional)
+
+Default: the users default userpic.
+
+=back
+
+=cut
 sub get_picid_from_keyword {
     my ( $u, $kw, $default ) = @_;
     $default ||= ref $u ? $u->{defaultpicid} : 0;
@@ -6239,15 +6364,61 @@ sub get_picid_from_keyword {
     my $info = LJ::isu( $u ) ? $u->get_userpic_info : undef;
     return $default unless $info;
 
-    my $pr = $info->{'kw'}{$kw};
+    my $pr = $info->{kw}{$kw};
     # normal keyword
     return $pr->{picid} if $pr->{picid};
 
     # the silly "pic#2343" thing when they didn't assign a keyword
     if ( $kw =~ /^pic\#(\d+)$/ ) {
         my $picid = $1;
-        return $picid if $info->{'pic'}{$picid};
-    }
+        return $picid if $info->{pic}{$picid};
+    }
+
+    return $default;
+}
+
+=head3 C<< $u->get_picid_from_mapid( $mapid, %opts ) >>
+
+Returns the picid for a given mapid.
+
+Arguments:
+
+=over 4
+
+=item mapid
+
+=back
+
+Additional options:
+
+=over 4
+
+=item default
+
+Default: the users default userpic.
+
+=item redir_callback
+
+Called if the mapping is redirected to another mapping with the following arguments
+
+( $u, $old_mapid, $new_mapid )
+
+=back
+
+=cut
+sub get_picid_from_mapid {
+    my ( $u, $mapid, %opts ) = @_;
+    my $default = $opts{default} || ref $u ? $u->{defaultpicid} : 0;
+    return $default unless $mapid;
+    return $default unless $u->userpic_have_mapid;
+
+    my $info = LJ::isu( $u ) ? $u->get_userpic_info : undef;
+    return $default unless $info;
+
+    $mapid = $u->resolve_mapid_redirects($mapid,%opts);
+    my $pr = $info->{mapid}{$mapid};
+
+    return $pr->{picid} if $pr->{picid};
 
     return $default;
 }
@@ -6340,10 +6511,13 @@ Maps a picid to a pic hashref.
 #       userid,
 #       "packed string", which expands to an array of {width=>..., ...}
 #       "packed string", which expands to { 'kw1' => id, 'kw2' => id, ...}
+#       series of 3 4-byte numbers, which expands to { mapid1 => id, mapid2 => id, ...}, as well as { mapid1 => mapid2 }
+#       "packed string", which expands to { 'kw1' => mapid, 'kw2' => mapid, ...}
 #       ]
 sub get_userpic_info {
     my ( $u, $opts ) = @_;
     return undef unless LJ::isu( $u ) && $u->clusterid;
+    my $mapped_icons = $u->userpic_have_mapid;
 
     # in the cache, cool, well unless it doesn't have comments or urls or descriptions
     # and we need them
@@ -6356,7 +6530,7 @@ sub get_userpic_info {
         return $cachedata if $good;
     }
 
-    my $VERSION_PICINFO = 3;
+    my $VERSION_PICINFO = 4;
 
     my $memkey = [$u->userid,"upicinf:$u->{'userid'}"];
     my ($info, $minfo);
@@ -6370,13 +6544,13 @@ sub get_userpic_info {
             # old data in the cache.  delete.
             LJ::MemCache::delete($memkey);
         } else {
-            my (undef, $picstr, $kwstr) = @$minfo;
+            my (undef, $picstr, $kwstr, $picmapstr, $kwmapstr) = @$minfo;
             $info = {
-                'pic' => {},
-                'kw' => {},
+                pic => {},
+                kw => {}
             };
             while (length $picstr >= 7) {
-                my $pic = { userid => $u->{'userid'} };
+                my $pic = { userid => $u->userid };
                 ($pic->{picid},
                  $pic->{width}, $pic->{height},
                  $pic->{state}) = unpack "NCCA", substr($picstr, 0, 7, '');
@@ -6389,12 +6563,37 @@ sub get_userpic_info {
                 my $kw = substr($kwstr, $pos, $nulpos-$pos);
                 my $id = unpack("N", substr($kwstr, $nulpos+1, 4));
                 $pos = $nulpos + 5; # skip NUL + 4 bytes.
-                $info->{kw}->{$kw} = $info->{pic}->{$id} if $info;
-            }
-        }
+                $info->{kw}->{$kw} = $info->{pic}->{$id};
+            }
+
+            if ( $mapped_icons ) {
+                if ( defined $picmapstr && defined $kwmapstr ) {
+                    $pos =  0;
+                    while ($pos < length($picmapstr)) {
+                        my ($mapid, $id, $redir) = unpack("NNN", substr($picmapstr, $pos, 12));
+                        $pos += 12; # 3 * 4 bytes.
+                        $info->{mapid}->{$mapid} = $info->{pic}{$id} if $id;
+                        $info->{map_redir}->{$mapid} = $redir if $redir;
+                    }
+                    
+                    $pos = $nulpos = 0;
+                    while (($nulpos = index($kwmapstr, "\0", $pos)) > 0) {
+                        my $kw = substr($kwmapstr, $pos, $nulpos-$pos);
+                        my $id = unpack("N", substr($kwmapstr, $nulpos+1, 4));
+                        $pos = $nulpos + 5; # skip NUL + 4 bytes.
+                        $info->{kwmap}->{$kw} = $id;
+                        $info->{mapkw}->{$id} = $kw || "pic#" . $info->{mapid}->{$id}->{picid};
+                    }
+                } else { # This user is on dversion 9, but the data isn't in memcache
+                         # so force a db load
+                    undef $info;
+                }
+            }
+        }
+
 
         # Load picture comments
-        if ( $opts->{load_comments} ) {
+        if ( $opts->{load_comments} && $info ) {
             my $commemkey = [$u->userid, "upiccom:" . $u->userid];
             my $comminfo = LJ::MemCache::get( $commemkey );
 
@@ -6464,10 +6663,10 @@ sub get_userpic_info {
     my %minfodesc;
     unless ($info) {
         $info = {
-            'pic' => {},
-            'kw' => {},
+            pic => {},
+            kw => {}
         };
-        my ($picstr, $kwstr);
+        my ($picstr, $kwstr, $predirstr, $kwmapstr);
         my $sth;
         my $dbcr = LJ::get_cluster_def_reader($u);
         my $db = @LJ::MEMCACHE_SERVERS ? LJ::get_db_writer() : LJ::get_db_reader();
@@ -6480,7 +6679,7 @@ sub get_userpic_info {
         while (my $pic = $sth->fetchrow_hashref) {
             next if $pic->{state} eq 'X'; # no expunged pics in list
             push @pics, $pic;
-            $info->{'pic'}->{$pic->{'picid'}} = $pic;
+            $info->{pic}->{$pic->{picid}} = $pic;
             $minfocom{int($pic->{picid})} = $pic->{comment}
                 if $opts->{load_comments} && $pic->{comment};
             $minfourl{int($pic->{picid})} = $pic->{url}
@@ -6493,20 +6692,47 @@ sub get_userpic_info {
         $picstr = join('', map { pack("NCCA", $_->{picid},
                                  $_->{width}, $_->{height}, $_->{state}) } @pics);
 
-        $sth = $dbcr->prepare( "SELECT k.keyword, m.picid FROM userpicmap2 m, userkeywords k ".
-                               "WHERE k.userid=? AND m.kwid=k.kwid AND m.userid=k.userid" );
+        if ( $mapped_icons ) {
+            $sth = $dbcr->prepare( "SELECT k.keyword, m.picid, m.mapid, m.redirect_mapid FROM userpicmap3 m LEFT JOIN userkeywords k ON ".
+                                "( m.userid=k.userid AND m.kwid=k.kwid ) WHERE m.userid=?" );
+        } else {
+            $sth = $dbcr->prepare( "SELECT k.keyword, m.picid FROM userpicmap2 m, userkeywords k ".
+                                "WHERE k.userid=? AND m.kwid=k.kwid AND m.userid=k.userid" );
+        }
         $sth->execute($u->{'userid'});
         my %minfokw;
-        while (my ($kw, $id) = $sth->fetchrow_array) {
-            next unless $info->{'pic'}->{$id};
+        my %picmap;
+        my %kwmap;
+        while (my ($kw, $id, $mapid, $redir) = $sth->fetchrow_array) {
+            my $skip_kw = 0;
+            if ( $mapped_icons ) {
+                $picmap{$mapid} = [ int($id), int($redir) ];
+                if ( $redir ) {
+                    $info->{map_redir}->{$mapid} = $redir;
+                } else {
+                    unless ( defined $kw ) {
+                        $skip_kw = 1;
+                        $kw = "pic#$id";
+                    }
+                    $info->{kwmap}->{$kw} = $kwmap{$kw} = $mapid;
+                    $info->{mapkw}->{$mapid} = $kw;
+                }
+            }
+            next if $skip_kw;
+            next unless $info->{pic}->{$id};
             next if $kw =~ /[\n\r\0]/;  # used to be a bug that allowed these to get in.
-            $info->{'kw'}->{$kw} = $info->{'pic'}->{$id};
+            $info->{kw}->{$kw} = $info->{pic}->{$id};
+            $info->{mapid}->{$mapid} = $info->{pic}->{$id} if $mapped_icons && $id;
             $minfokw{$kw} = int($id);
         }
         $kwstr = join('', map { pack("Z*N", $_, $minfokw{$_}) } keys %minfokw);
+        if ( $mapped_icons ) {
+            $predirstr = join('', map { pack("NNN", $_, @{ $picmap{$_} } ) } keys %picmap);
+            $kwmapstr = join('', map { pack("Z*N", $_, $kwmap{$_}) } keys %kwmap);
+        }
 
         $memkey = [$u->{'userid'},"upicinf:$u->{'userid'}"];
-        $minfo = [ $VERSION_PICINFO, $picstr, $kwstr ];
+        $minfo = [ $VERSION_PICINFO, $picstr, $kwstr, $predirstr, $kwmapstr ];
         LJ::MemCache::set($memkey, $minfo);
 
         if ( $opts->{load_comments} ) {
@@ -6558,7 +6784,7 @@ sub get_userpic_kw_map {
     foreach my $keyword ( keys %{$picinfo->{kw}} ) {
         my $picid = $picinfo->{kw}->{$keyword}->{picid};
         $keywords->{$picid} = [] unless $keywords->{$picid};
-        push @{$keywords->{$picid}}, $keyword if ( $keyword && $picid );
+        push @{$keywords->{$picid}}, $keyword if ( $keyword && $picid && $keyword !~ m/^pic\#(\d+)$/ );
     }
 
     return $u->{picid_kw_map} = $keywords;
@@ -6587,6 +6813,59 @@ sub mogfs_userpic_key {
     return "up:" . $self->userid . ":$picid";
 }
 
+=head3 C<< $u->resolve_mapid_redirects( $mapid, %opts ) >>
+
+Resolve any mapid redirect, guarding against any redirect loops.
+
+Returns: new map id, or 0 if the mapping cannot be resolved.
+
+Arguments:
+
+=over 4
+
+=item mapid
+
+=back
+
+Additional options:
+
+=over 4
+
+=item redir_callback
+
+Called if the mapping is redirected to another mapping with the following arguments
+
+( $u, $old_mapid, $new_mapid )
+
+=back
+
+=cut
+sub resolve_mapid_redirects {
+    my ( $u, $mapid, %opts ) = @_;
+
+    my $info = LJ::isu( $u ) ? $u->get_userpic_info : undef;
+    return 0 unless $info;
+
+    my %seen = ( $mapid => 1 );
+    my $orig_id = $mapid;
+
+    while ( $info->{map_redir}->{ $mapid } ) {
+        $orig_id = $mapid;
+        $mapid = $info->{map_redir}->{ $mapid };
+
+        # To implement lazy updating or the like
+        $opts{redir_callback}->($u, $orig_id, $mapid) if $opts{redir_callback};
+
+        # This should never happen, but am checking it here mainly in case
+        # never *does* happen, so we don't hang the web process with an endless loop.
+        if ( $seen{$mapid}++ ) {
+            warn("userpicmap3 redirectloop for " . $u->id . " on mapid " . $mapid);
+            return 0;
+        }
+    }
+
+    return $mapid;
+}
 
 =head3 C<< $u->userpic >>
 
@@ -6599,6 +6878,15 @@ sub userpic {
     return LJ::Userpic->new($u, $u->{defaultpicid});
 }
 
+=head3 C<< $u->userpic_have_mapid >>
+
+Returns true if the userpicmap keyword mappings have a mapid column ( dversion 9 or higher )
+
+=cut
+# FIXME: This probably should be userpics_use_mapid
+sub userpic_have_mapid {
+    return $_[0]->dversion >= 9;
+}
 
 
 =head3 C<< $u->userpic_quota >>
@@ -6613,7 +6901,23 @@ sub userpic_quota {
     return $quota;
 }
 
-
+# Intentionally no POD here.
+# This is an internal helper method
+# takes a $kwid and $picid ( either can be undef )
+# and creates a mapid row for it
+sub _create_mapid {
+    my ( $u, $kwid, $picid ) = @_;
+    return 0 unless $u->userpic_have_mapid;
+
+    my $mapid = LJ::alloc_user_counter($u,'Y');
+    $u->do( "INSERT INTO userpicmap3 (userid, mapid, kwid, picid) VALUES (?,?,?,?)", undef, $u->id, $mapid, $kwid, $picid);
+    return 0 if $u->err;
+
+    LJ::Userpic->delete_cache($u);
+    $u->clear_userpic_kw_map;
+
+    return $mapid;
+}
 
 ########################################################################
 ###  99. Miscellaneous Legacy Items
@@ -7390,7 +7694,7 @@ sub unset_remote
 #       'Q' == Notification Inbox,
 #       'D' == 'moDule embed contents', 'I' == Import data block
 #       'Z' == import status item, 'X' == eXternal account
-#       'F' == filter id
+#       'F' == filter id, 'Y' = pic/keYword mapping id
 #
 sub alloc_user_counter
 {
@@ -7399,7 +7703,7 @@ sub alloc_user_counter
 
     ##################################################################
     # IF YOU UPDATE THIS MAKE SURE YOU ADD INITIALIZATION CODE BELOW #
-    return undef unless $dom =~ /^[LTMPSRKCOVEQGDIZXF]$/;            #
+    return undef unless $dom =~ /^[LTMPSRKCOVEQGDIZXFY]$/;           #
     ##################################################################
 
     my $dbh = LJ::get_db_writer();
@@ -7520,6 +7824,9 @@ sub alloc_user_counter
     } elsif ($dom eq "F") {
         $newmax = $u->selectrow_array("SELECT MAX(filterid) FROM watch_filters WHERE userid=?",
                                       undef, $uid);
+    } elsif ($dom eq "Y") {
+        $newmax = $u->selectrow_array("SELECT MAX(mapid) FROM userpicmap3 WHERE userid=?",
+                                         undef, $uid);
     } else {
         die "No user counter initializer defined for area '$dom'.\n";
     }
diff -r ebea5b13e8a4 -r f897918203ac cgi-bin/LJ/Userpic.pm
--- a/cgi-bin/LJ/Userpic.pm	Sat Oct 02 00:29:19 2010 +0800
+++ b/cgi-bin/LJ/Userpic.pm	Sat Oct 02 01:27:29 2010 +0800
@@ -60,6 +60,14 @@ sub reset_singletons {
 sub reset_singletons {
     %singletons = ();
 }
+
+=head1 NAME
+
+LJ::Userpic
+
+=head1 Class Methods
+
+=cut
 
 # LJ::Userpic constructor. Returns a LJ::Userpic object.
 # Return existing with userid and picid populated, or make new.
@@ -148,8 +156,12 @@ sub new_from_row {
     return $self;
 }
 
-sub new_from_keyword
-{
+=head2 C<< $class->new_from_keyword( $u, $kw ) >>
+
+Returns the LJ::Userpic for the given keyword
+
+=cut
+sub new_from_keyword {
     my ( $class, $u, $kw ) = @_;
     return undef unless LJ::isu( $u );
 
@@ -158,7 +170,24 @@ sub new_from_keyword
     return $picid ? $class->new( $u, $picid ) : undef;
 }
 
-# instance methods
+
+=head2 C<< $class->new_from_mapid( $u, $mapid ) >>
+
+Returns the LJ::Userpic for the given mapid
+
+=cut
+sub new_from_mapid {
+    my ( $class, $u, $mapid ) = @_;
+    return undef unless LJ::isu( $u );
+
+    my $picid = $u->get_picid_from_mapid( $mapid );
+
+    return $picid ? $class->new( $u, $picid ) : undef;
+}
+
+=head1 Instance Methods
+
+=cut
 
 sub valid {
     return defined $_[0]->state;
@@ -934,11 +963,14 @@ sub delete {
 
     # userpic keywords
     eval {
-        $u->do( "DELETE FROM userpicmap2 WHERE userid=? " .
-                "AND picid=?", undef, $u->userid, $picid ) or die;
-        $u->do( "DELETE FROM userpic2 WHERE picid=? AND userid=?",
-                undef, $picid, $u->userid ) or die;
-        };
+        if ( $u->userpic_have_mapid ) {
+            $u->do( "DELETE FROM userpicmap3 WHERE userid = ? AND picid = ? AND kwid=NULL", undef, $u->userid, $picid ) or die;
+            $u->do( "UPDATE userpicmap3 SET picid=NULL WHERE userid=? AND picid=?", undef, $u->userid, $picid ) or die;
+        } else {
+            $u->do( "DELETE FROM userpicmap2 WHERE userid=? AND picid=?", undef, $u->userid, $picid ) or die;
+        }
+        $u->do( "DELETE FROM userpic2 WHERE picid=? AND userid=?", undef, $picid, $u->userid ) or die;
+    };
     $fail->() if $@;
 
     $u->log_event('delete_userpic', { picid => $picid });
@@ -1005,15 +1037,31 @@ sub set_keywords {
     @keywords = grep { !/^pic\#\d+$/ } grep { s/^\s+//; s/\s+$//; $_; } @keywords;
 
     my $u = $self->owner;
+    my $have_mapid = $u->userpic_have_mapid;
+
     my $sth;
     my $dbh;
 
-    $sth = $u->prepare( "SELECT kwid FROM userpicmap2 WHERE userid=? AND picid=?" );
+    if ( $have_mapid ) {
+        $sth = $u->prepare( "SELECT kwid FROM userpicmap3 WHERE userid=? AND picid=?" );
+    } else {
+        $sth = $u->prepare( "SELECT kwid FROM userpicmap2 WHERE userid=? AND picid=?" );
+    }
     $sth->execute( $u->userid, $self->id );
 
     my %exist_kwids;
     while (my ($kwid) = $sth->fetchrow_array) {
         $exist_kwids{$kwid} = 1;
+    }
+
+    my %kwid_to_mapid;
+    if ( $have_mapid ) {
+        $sth = $u->prepare( "SELECT mapid, kwid FROM userpicmap3 WHERE userid=?" );
+        $sth->execute( $u->userid );
+
+        while (my ($mapid, $kwid) = $sth->fetchrow_array) {
+            $kwid_to_mapid{$kwid} = $mapid;
+        }
     }
 
     my (@bind, @data, @kw_errors);
@@ -1030,23 +1078,39 @@ sub set_keywords {
         }
 
         unless (delete $exist_kwids{$kwid}) {
-            push @bind, '(?, ?, ?)';
-            push @data, $u->{'userid'}, $kwid, $picid;
+            if ( $have_mapid ) {
+                $kwid_to_mapid{$kwid} ||= LJ::alloc_user_counter( $u, 'Y' );
+
+                push @bind, '(?, ?, ?, ?)';
+                push @data, $u->userid, $kwid_to_mapid{$kwid}, $kwid, $picid;
+            } else {
+                push @bind, '(?, ?, ?)';
+                push @data, $u->userid, $kwid, $picid;
+            }
         }
     }
 
     LJ::Userpic->delete_cache($u);
 
     foreach my $kwid (keys %exist_kwids) {
-        $u->do("DELETE FROM userpicmap2 WHERE userid=? AND picid=? AND kwid=?", undef, $u->{userid}, $self->id, $kwid);
+        if ( $have_mapid ) {
+            $u->do("UPDATE userpicmap3 SET picid=NULL WHERE userid=? AND picid=? AND kwid=?", undef, $u->id, $self->id, $kwid);
+        } else {
+            $u->do("DELETE FROM userpicmap2 WHERE userid=? AND picid=? AND kwid=?", undef, $u->id, $self->id, $kwid);
+        }
     }
 
     # save data if any
     if (scalar @data) {
         my $bind = join(',', @bind);
 
-        $u->do( "REPLACE INTO userpicmap2 (userid, kwid, picid) VALUES $bind",
-                undef, @data );
+        if ( $have_mapid ) {
+            $u->do( "REPLACE INTO userpicmap3 (userid, mapid, kwid, picid) VALUES $bind",
+                    undef, @data );
+        } else {            
+            $u->do( "REPLACE INTO userpicmap2 (userid, kwid, picid) VALUES $bind",
+                    undef, @data );
+        }
     }
 
     # clear the userpic-keyword map.
diff -r ebea5b13e8a4 -r f897918203ac cgi-bin/ljlib.pl
--- a/cgi-bin/ljlib.pl	Sat Oct 02 00:29:19 2010 +0800
+++ b/cgi-bin/ljlib.pl	Sat Oct 02 01:27:29 2010 +0800
@@ -126,7 +126,7 @@ sub END { LJ::end_request(); }
                     "embedcontent", "usermsg", "usermsgtext", "usermsgprop",
                     "notifyarchive", "notifybookmarks", "pollprop2", "embedcontent_preview",
                     "logprop_history", "import_status", "externalaccount",
-                    "content_filters", "content_filter_data",
+                    "content_filters", "content_filter_data", "userpicmap3",
                     );
 
 # keep track of what db locks we have out
diff -r ebea5b13e8a4 -r f897918203ac cgi-bin/ljprotocol.pl
--- a/cgi-bin/ljprotocol.pl	Sat Oct 02 00:29:19 2010 +0800
+++ b/cgi-bin/ljprotocol.pl	Sat Oct 02 01:27:29 2010 +0800
@@ -1132,6 +1132,7 @@ sub postevent
         $flags->{noauth} = 1;
         $flags->{usejournal_okay} = 1;
         $flags->{no_xpost} = 1;
+        $flags->{create_unknown_picture_mapid} = 1;
     }
 
     return undef unless LJ::Hooks::run_hook('post_noauth', $req) || authenticate($req, $err, $flags);
@@ -1221,6 +1222,12 @@ sub postevent
 
     return undef
         unless common_event_validation($req, $err, $flags);
+
+    # now we can move over to picture_mapid instead of picture_keyword if appropriate
+    if ( $req->{props} && $req->{props}->{picture_keyword} && $u->userpic_have_mapid ) {
+        $req->{props}->{picture_mapid} = $u->get_mapid_from_keyword( $req->{props}->{picture_keyword}, create => $flags->{create_unknown_picture_mapid} || 0 );
+        delete $req->{props}->{picture_keyword};
+    }
 
     # confirm we can add tags, at least
     return fail($err, 312)
@@ -1886,6 +1893,12 @@ sub editevent
     return undef
         unless common_event_validation($req, $err, $flags);
 
+    # now we can move over to picture_mapid instead of picture_keyword if appropriate
+    if ( $req->{props} && $req->{props}->{picture_keyword} && $u->userpic_have_mapid ) {
+        $req->{props}->{picture_mapid} = $u->get_mapid_from_keyword( $req->{props}->{picture_keyword}, create => $flags->{create_unknown_picture_mapid} || 0 );
+        delete $req->{props}->{picture_keyword};
+    }
+
     ## handle meta-data (properties)
     my %props_byname = ();
     foreach my $key (keys %{$req->{'props'}}) {
@@ -2409,10 +2422,15 @@ sub getevents
 
         # if they want subjects to be events, replace event
         # with subject when requested.
-        if ($req->{'prefersubject'} && length($t->[0])) {
+        if ($req->{prefersubject} && length($t->[0])) {
             $t->[1] = $t->[0];  # event = subject
             $t->[0] = undef;    # subject = undef
         }
+
+        # re-generate the picture_keyword prop for the returned data, as a mapid will mean nothing
+        my $pu = $uowner;
+        $pu = LJ::load_user( $evt->{poster} ) if $evt->{poster};
+        $evt->{props}->{picture_keyword} = $pu->get_keyword_from_mapid( $evt->{props}->{picture_mapid} ) if $pu->userpic_have_mapid;
 
         # now that we have the subject, the event and the props,
         # auto-translate them to UTF-8 if they're not in UTF-8.
@@ -3068,17 +3086,18 @@ sub list_pickws
     my %seen;  # mashifiedptr -> 1
 
     # FIXME: should be a utf-8 sort
-    foreach my $kw (sort keys %{$pi->{'kw'}}) {
-        my $pic = $pi->{'kw'}{$kw};
+    foreach my $kw ( sort keys %{$pi->{kw}} ) {
+        my $pic = $pi->{kw}{$kw};
         $seen{$pic} = 1;
-        next if $pic->{'state'} eq "I";
-        push @res, [ $kw, $pic->{'picid'} ];
+        next if $pic->{state} eq "I";
+        push @res, [ $kw, $pic->{picid} ];
     }
 
     # now add all the pictures that don't have a keyword
-    foreach my $picid (keys %{$pi->{'pic'}}) {
-        my $pic = $pi->{'pic'}{$picid};
+    foreach my $picid ( keys %{$pi->{pic}} ) {
+        my $pic = $pi->{pic}{$picid};
         next if $seen{$pic};
+        next if $pic->{state} eq "I";
         push @res, [ "pic#$picid", $picid ];
     }
 
@@ -3852,6 +3871,7 @@ sub editevent
     $res->{'itemid'} = $rs->{'itemid'};
     $res->{'anum'} = $rs->{'anum'} if defined $rs->{'anum'};
     $res->{'url'} = $rs->{'url'} if defined $rs->{'url'};
+
     return 1;
 }
 
diff -r ebea5b13e8a4 -r f897918203ac htdocs/admin/entryprops.bml
--- a/htdocs/admin/entryprops.bml	Sat Oct 02 00:29:19 2010 +0800
+++ b/htdocs/admin/entryprops.bml	Sat Oct 02 01:27:29 2010 +0800
@@ -59,8 +59,10 @@ body<=
         }
     }
 
+    my $pu = $entry->poster;
+
     $ret .= "<strong>Subject</strong>: <a href=" . $entry->url . ">" . $subject . "</a><br />";
-    $ret .= "<strong>Poster</strong>: " . $entry->poster->ljuser_display . "<br />";
+    $ret .= "<strong>Poster</strong>: " . $pu->ljuser_display . "<br />";
     $ret .= "<strong>Journal</strong>: " . $entry->journal->ljuser_display . "<br />";
     $ret .= "<strong>Security</strong>: " . $security . " ";
     $ret .= "(journal wide minsecurity: " . ($entry->journal->prop("newpost_minsecurity") || "public") . ")<br />";
@@ -82,7 +84,7 @@ body<=
             
             # render xpost prop into human readable form
             if ( $prop eq "xpost" || $prop eq "xpostdetail" ) {
-                my %external_accounts_map = map { $_->acctid => $_->servername } DW::External::Account->get_external_accounts( $entry->poster );
+                my %external_accounts_map = map { $_->acctid => $_->servername } DW::External::Account->get_external_accounts( $pu );
 
                 # FIXME: temporary; trying to figure out when this is undef
                 my $xpost_prop = $props{$prop};
@@ -100,6 +102,20 @@ body<=
                 # FIXME: temporary
                 $props{$prop} .= "raw information about $prop - <input type='text' value='$xpost_prop' />" 
                     unless $xpost_hash;
+            } elsif ( $prop eq 'picture_mapid' && $pu->userpic_have_mapid ) {
+                my $result = "$props{$prop} -> ";
+                my $kw = $pu->get_keyword_from_mapid( $props{$prop},
+                            redir_callback => sub {
+                                $result .= "$_[2] -> ";
+                            });
+                $result .= $kw;
+                my $picid = $pu->get_picid_from_keyword($kw,-1);
+                if ( $picid == -1 ) {
+                    $result .= " ( not assigned to an icon )";
+                } else {
+                    $result .= " ( assigned to an icon )";
+                }
+                $props{$prop} = $result;
             }
 
             $extra = "<br /><small>$p->{des}</small>";
diff -r ebea5b13e8a4 -r f897918203ac htdocs/talkpost.bml
--- a/htdocs/talkpost.bml	Sat Oct 02 00:29:19 2010 +0800
+++ b/htdocs/talkpost.bml	Sat Oct 02 01:27:29 2010 +0800
@@ -53,8 +53,9 @@ body<=
         $FORM{subject} = $comment->subject_orig;
         $FORM{body} = $comment->body_orig;
         $FORM{subjecticon} = $comment->prop('subjecticon');
-        $FORM{prop_picture_keyword} = $comment->prop('picture_keyword');
         $FORM{prop_opt_preformatted} = $comment->prop('opt_preformatted');
+
+        $FORM{prop_picture_keyword} = $comment->userpic_kw;
     }
 
     if ($uri =~ m!/(\d+)\.html$!) {
diff -r ebea5b13e8a4 -r f897918203ac htdocs/talkread.bml
--- a/htdocs/talkread.bml	Sat Oct 02 00:29:19 2010 +0800
+++ b/htdocs/talkread.bml	Sat Oct 02 01:27:29 2010 +0800
@@ -239,7 +239,7 @@ body<=
     $ret .= "<p>";
     $ret .= "<table id='poster'><tr>";
 
-    my $userpic = $entry->userpic;
+    my ( $userpic, $kw ) = $entry->userpic;
     LJ::Hooks::run_hook('notify_event_displayed', $entry);
 
     # Build the userpic image tag for the entry's userpic
@@ -249,7 +249,7 @@ body<=
         my $apost = "</a></td>";
 
         # for each image, get the html imgtag
-        $ret .= $apre . $userpic->imgtag( keyword => $props->{picture_keyword}, user => $up ) . $apost;
+        $ret .= $apre . $userpic->imgtag( keyword => $kw, user => $up ) . $apost;
     }
 
     $ret .= "<td class='attrib' valign='bottom'>";
@@ -498,7 +498,7 @@ body<=
                    # get the picture keyword from the comment properties
                    # get the comment poster as well
                    my %kwopts = (
-                      keyword => $post->{props}->{picture_keyword},
+                      keyword => $post->{pickw},
                       user => $upost,
                    );
 
--------------------------------------------------------------------------------

Post a comment in response:

This account has disabled anonymous posting.
If you don't have an account you can create one now.
No Subject Icon Selected
More info about formatting

If you are unable to use this captcha for any reason, please contact us by email at support@dreamwidth.org