[dw-free] Implement v-gifts
[commit: http://hg.dwscoalition.org/dw-free/rev/c4063e1602c6]
http://bugs.dwscoalition.org/show_bug.cgi?id=215
Backend modules and admin pages.
Patch by
kareila.
Files modified:
http://bugs.dwscoalition.org/show_bug.cgi?id=215
Backend modules and admin pages.
Patch by
Files modified:
- bin/upgrading/base-data.sql
- bin/upgrading/en.dat
- bin/upgrading/update-db-general.pl
- cgi-bin/Apache/LiveJournal.pm
- cgi-bin/DW/Controller/Admin.pm
- cgi-bin/DW/VirtualGift.pm
- cgi-bin/LJ/Event/VgiftApproved.pm
- cgi-bin/ljdefaults.pl
- cgi-bin/ljlib.pl
- htdocs/admin/vgifts/inactive.bml
- htdocs/admin/vgifts/inactive.bml.text
- htdocs/admin/vgifts/index.bml
- htdocs/admin/vgifts/index.bml.text
- htdocs/admin/vgifts/tags.bml
- htdocs/admin/vgifts/tags.bml.text
- views/admin/index.tt.text
--------------------------------------------------------------------------------
diff -r 4c7678ebddbf -r c4063e1602c6 bin/upgrading/base-data.sql
--- a/bin/upgrading/base-data.sql Thu Oct 28 11:15:07 2010 +0800
+++ b/bin/upgrading/base-data.sql Thu Oct 28 12:23:03 2010 +0800
@@ -566,6 +566,8 @@ UPDATE priv_list SET des='Allows a user
UPDATE priv_list SET des='Allows a user to modify bans with the sysban mechanism. arg=A specific ban type the user can modify, or \"*\" for all ban type.',is_public='0',privname='Modify System Bans',scope='general' WHERE privcode='sysban';
INSERT IGNORE INTO priv_list (des, is_public, privcode, privname, scope) VALUES ('Allows a user to edit site text in a given language. arg=Unique language code, optionally appended by |domainid.domaincode', '1', 'translate', 'Translate/Update Text', 'general');
UPDATE priv_list SET des='Allows a user to edit site text in a given language. arg=Unique language code, optionally appended by |domainid.domaincode',is_public='1',privname='Translate/Update Text',scope='general' WHERE privcode='translate';
+INSERT IGNORE INTO priv_list (des, is_public, privcode, privname, scope) VALUES ('Allows a user to add/edit vgifts. arg=Tag in case restricting priv to a particular category is needed, or "*" for all tags.', '1', 'vgifts', 'Virtual Gifts', 'general');
+UPDATE priv_list SET des='Allows a user to add/edit vgifts. arg=Tag in case restricting priv to a particular category is needed, or "*" for all tags.',is_public='1',privname='Virtual Gifts',scope='general' WHERE privcode='vgifts';
INSERT IGNORE INTO ratelist (des, name) VALUES ('Logged when a user adds someone to their Friends list', 'addfriend');
UPDATE ratelist SET des='Logged when a user adds someone to their Friends list' WHERE name='addfriend';
INSERT IGNORE INTO ratelist (des, name) VALUES ('Logged when a user creates a community.', 'commcreate');
diff -r 4c7678ebddbf -r c4063e1602c6 bin/upgrading/en.dat
--- a/bin/upgrading/en.dat Thu Oct 28 11:15:07 2010 +0800
+++ b/bin/upgrading/en.dat Thu Oct 28 12:23:03 2010 +0800
@@ -1983,6 +1983,20 @@ event.xpost.success.content=Crosspost to
event.xpost.success.title=Crosspost of <a href="[[entryurl]]">[[entrydesc]]</a> to [[accountname]] action successful.
+event.vgift.approved.actions=<a [[aopts]]>View Gift</a>
+
+event.vgift.approved.content.N=[[admin]] has rejected your virtual gift submission "[[vgift]]".
+
+event.vgift.approved.content.Y=[[admin]] has approved your virtual gift submission "[[vgift]]".
+
+event.vgift.approved.msg.N=Sorry, your proposed virtual gift named "[[vgift]]" was not approved.
+
+event.vgift.approved.msg.Y=Congratulations, your proposed virtual gift named "[[vgift]]" was approved.
+
+event.vgift.approved.reason=The reviewer, [[admin]], included the following comments:
+
+event.vgift.notfound=The specified virtual gift could not be found.
+
fckland.ljimage=Insert/Edit Image
fcklang.cutcontents=Type your cut contents here.
@@ -3986,6 +4000,54 @@ userpic.link=userpic
userpic.text=[[user]], this is what you currently look like to your friends: [[empty]]<br />Boooring. Be classy and upload a [[link]].
+vgift.display.cost.free=Free
+
+vgift.display.cost.points=[[cost]] [[?cost|point|points]]
+
+vgift.display.createdby=Created by [[user]] [[ago]]
+
+vgift.display.creatorlist.counts=[[user]] ([[approved]] approved, [[active]] active)
+
+vgift.display.label.cost=Cost:
+
+vgift.display.label.featured=Featured:
+
+vgift.display.label.tags=Tags:
+
+vgift.display.linktext.delete=[Delete]
+
+vgift.display.linktext.review=[Review]
+
+vgift.display.linktext.viewedit=[View/Edit]
+
+vgift.display.linktext.viewgifts=[View Gifts]
+
+vgift.error.create.noname=Name must be specified
+
+vgift.error.create.samename=Name already in use
+
+vgift.error.init.alloc=Could not allocate new vgiftid
+
+vgift.error.init.reuse=Cannot reuse vgift objects
+
+vgift.error.loadpic=<b>No [[size]] image found.</b>
+
+vgift.error.savepics=Unable to save [[size]] image
+
+vgift.error.tags.create=Cannot create new tag: [[tag]]
+
+vgift.error.tags.invalid=Attempt to use invalid tags: [[taglist]]
+
+vgift.error.tags.novalidtags=No valid tags!
+
+vgift.error.validate.value=Invalid value for [[prop]]
+
+vgift.error.validate.property=Invalid property '[[key]]'
+
+vgift.error.validate.text=Invalid text encoding for [[prop]]
+
+vgift.error.validate.value=Invalid value for [[prop]]
+
web.ads.advertisement=<a [[aopts]]>Advertisement</a>
web.ads.advertisement_nolink=Advertisement
diff -r 4c7678ebddbf -r c4063e1602c6 bin/upgrading/update-db-general.pl
--- a/bin/upgrading/update-db-general.pl Thu Oct 28 11:15:07 2010 +0800
+++ b/bin/upgrading/update-db-general.pl Thu Oct 28 12:23:03 2010 +0800
@@ -17,6 +17,49 @@ use strict;
use strict;
mark_clustered(@LJ::USER_TABLES);
+
+register_tablecreate("vgift_ids", <<'EOC');
+CREATE TABLE vgift_ids (
+ vgiftid INT UNSIGNED NOT NULL PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ created_t INT UNSIGNED NOT NULL, #unixtime
+ creatorid INT UNSIGNED NOT NULL DEFAULT 0,
+ active ENUM('Y','N') NOT NULL DEFAULT 'N',
+ featured ENUM('Y','N') NOT NULL DEFAULT 'N',
+ custom ENUM('Y','N') NOT NULL DEFAULT 'N',
+ approved ENUM('Y','N'),
+ approved_by INT UNSIGNED,
+ approved_why MEDIUMTEXT,
+ description MEDIUMTEXT,
+ cost INT UNSIGNED NOT NULL DEFAULT 0,
+ mime_small VARCHAR(255),
+ mime_large VARCHAR(255),
+
+ UNIQUE KEY (name)
+)
+EOC
+
+register_tablecreate("vgift_tags", <<'EOC');
+CREATE TABLE vgift_tags (
+ tagid INT UNSIGNED NOT NULL,
+ vgiftid INT UNSIGNED NOT NULL,
+
+ PRIMARY KEY (tagid, vgiftid),
+ INDEX (vgiftid),
+ INDEX (tagid)
+)
+EOC
+
+register_tablecreate("vgift_tagpriv", <<'EOC');
+CREATE TABLE vgift_tagpriv (
+ tagid INT UNSIGNED NOT NULL,
+ prlid SMALLINT UNSIGNED NOT NULL,
+ arg VARCHAR(40),
+
+ PRIMARY KEY (tagid, prlid, arg),
+ INDEX (tagid)
+)
+EOC
register_tablecreate("authactions", <<'EOC');
CREATE TABLE authactions (
diff -r 4c7678ebddbf -r c4063e1602c6 cgi-bin/Apache/LiveJournal.pm
--- a/cgi-bin/Apache/LiveJournal.pm Thu Oct 28 11:15:07 2010 +0800
+++ b/cgi-bin/Apache/LiveJournal.pm Thu Oct 28 12:23:03 2010 +0800
@@ -39,6 +39,7 @@ use LJ::URI;
use LJ::URI;
use DW::Routing;
use DW::Template;
+use DW::VirtualGift;
BEGIN {
$LJ::OPTMOD_ZLIB = eval "use Compress::Zlib (); 1;";
@@ -920,6 +921,8 @@ sub trans
# userpic
return userpic_trans($r) if $uri =~ m!^/userpic/!;
+ return vgift_trans($r) if $uri =~ m!^/vgift/!;
+
# front page journal
if ($LJ::FRONTPAGE_JOURNAL) {
my $view = $determine_view->($LJ::FRONTPAGE_JOURNAL, "front", $uri);
@@ -1127,8 +1130,10 @@ sub userpic_content
my $size;
my $send_headers = sub {
- $r->content_type($mime);
- $r->headers_out->{"Content-length"} = $size+0;
+ $size = $_[0] if @_;
+ $size ||= 0;
+ $r->content_type( $mime );
+ $r->headers_out->{"Content-length"} = $size + 0;
$r->headers_out->{"Cache-Control"} = "no-transform";
$r->headers_out->{"Last-Modified"} = LJ::time_to_http($lastmod);
};
@@ -1146,47 +1151,8 @@ sub userpic_content
# For dversion 7+ and mogilefs userpics, follow this path
if ( $pic->in_mogile ) {
my $key = $u->mogfs_userpic_key( $picid );
-
- if ( !$LJ::REPROXY_DISABLE{userpics} &&
- $r->headers_in->{'X-Proxy-Capabilities'} &&
- $r->headers_in->{'X-Proxy-Capabilities'} =~ m{\breproxy-file\b}i )
- {
- my $memkey = [$picid, "mogp.up.$picid"];
-
- my $zone = $r->headers_in->{'X-MogileFS-Explicit-Zone'} || undef;
- $memkey->[1] .= ".$zone" if $zone;
-
- my $cache_for = $LJ::MOGILE_PATH_CACHE_TIMEOUT || 3600;
-
- my $paths = LJ::MemCache::get($memkey);
- unless ($paths) {
- my @paths = LJ::mogclient()->get_paths( $key, { noverify => 1, zone => $zone });
- $paths = \@paths;
- LJ::MemCache::add($memkey, $paths, $cache_for) if @paths;
- }
-
- # reproxy url
- if ($paths->[0] =~ m/^http:/) {
- $r->headers_out->{'X-REPROXY-CACHE-FOR'} = "$cache_for; Last-Modified Content-Type";
- $r->headers_out->{'X-REPROXY-URL'} = join(' ', @$paths);
- }
-
- # reproxy file
- else {
- $r->headers_out->{'X-REPROXY-FILE'} = $paths->[0];
- }
-
- $send_headers->();
- }
-
- else {
- my $data = LJ::mogclient()->get_file_data( $key );
- return NOT_FOUND unless $data;
- $size = length $$data;
- $send_headers->();
- $r->print( $$data ) unless $r->header_only;
- }
-
+ my $memkey = [$picid, "mogp.up.$picid"];
+ mogile_fetch( $r, $key, $memkey, 'userpics', $send_headers );
return OK;
}
@@ -1262,6 +1228,54 @@ sub files_trans
return OK;
}
return 404;
+}
+
+sub vgift_trans
+{
+ my $r = shift;
+ return 404 unless $r->uri =~ m!^/vgift/(\d+)/(\w+)$!;
+ my ( $picid, $picsize ) = ( $1, $2 );
+ return 404 unless $picsize =~ /^(?:small|large)$/;
+
+ $r->notes->{codepath} = "img.vgift";
+
+ # we can safely do this without checking
+ # unless we're using the admin interface
+ return HTTP_NOT_MODIFIED if $r->headers_in->{'If-Modified-Since'}
+ && $r->headers_in->{'Referer'} !~ m!^\Q$LJ::SITEROOT\E$/admin/!;
+
+ $RQ{picid} = $picid;
+ $RQ{picsize} = $picsize;
+
+ $r->handler( "perl-script" );
+ $r->push_handlers( PerlResponseHandler => \&vgift_content );
+ return OK;
+}
+
+sub vgift_content
+{
+ my $r = shift;
+ my $picid = $RQ{picid};
+ my $picsize = $RQ{picsize};
+
+ my $vg = DW::VirtualGift->new( $picid );
+ my $mime = $vg->mime_type( $picsize );
+ return NOT_FOUND unless $mime;
+
+ my $size;
+
+ my $send_headers = sub {
+ $size = $_[0] if @_;
+ $size ||= 0;
+ $r->content_type( $mime );
+ $r->headers_out->{"Content-length"} = $size + 0;
+ $r->headers_out->{"Cache-Control"} = "no-transform";
+ };
+
+ my $key = $vg->img_mogkey( $picsize );
+ my $memkey = $vg->img_memkey( $picsize ); #[$picid, "mogp.vg.$picsize.$picid"];
+ mogile_fetch( $r, $key, $memkey, 'vgifts', $send_headers );
+ return OK;
}
sub journal_content
@@ -1781,6 +1795,45 @@ sub anti_squatter
}
+sub mogile_fetch {
+ my ( $r, $key, $memkey, $class, $send_headers ) = @_;
+
+ if ( !$LJ::REPROXY_DISABLE{$class} &&
+ $r->headers_in->{'X-Proxy-Capabilities'} &&
+ $r->headers_in->{'X-Proxy-Capabilities'} =~ m{\breproxy-file\b}i ) {
+
+ my $zone = $r->headers_in->{'X-MogileFS-Explicit-Zone'} || undef;
+ $memkey->[1] .= ".$zone" if $zone;
+
+ my $cache_for = $LJ::MOGILE_PATH_CACHE_TIMEOUT || 3600;
+
+ my $paths = LJ::MemCache::get( $memkey );
+ unless ( $paths ) {
+ # load and add to memcache
+ my @paths = LJ::mogclient()->get_paths( $key, { noverify => 1, zone => $zone } );
+ $paths = \@paths;
+ LJ::MemCache::add( $memkey, $paths, $cache_for ) if @paths;
+ }
+
+ if ( defined $paths->[0] && $paths->[0] =~ m/^http:/ ) {
+ # reproxy url
+ $r->headers_out->{'X-REPROXY-CACHE-FOR'} = "$cache_for; Last-Modified Content-Type";
+ $r->headers_out->{'X-REPROXY-URL'} = join( ' ', @$paths );
+ } else {
+ # reproxy file
+ $r->headers_out->{'X-REPROXY-FILE'} = $paths->[0];
+ }
+
+ $send_headers->();
+
+ } else { # no reproxy
+ my $data = LJ::mogclient()->get_file_data( $key );
+ return NOT_FOUND unless $data;
+ $send_headers->( length $$data );
+ $r->print( $$data ) unless $r->header_only;
+ }
+}
+
package LJ::Protocol;
use Encode();
diff -r 4c7678ebddbf -r c4063e1602c6 cgi-bin/DW/Controller/Admin.pm
--- a/cgi-bin/DW/Controller/Admin.pm Thu Oct 28 11:15:07 2010 +0800
+++ b/cgi-bin/DW/Controller/Admin.pm Thu Oct 28 12:23:03 2010 +0800
@@ -109,6 +109,10 @@ DW::Controller::Admin->_register_admin_p
'.admin.translate.link', '.admin.translate.text' ],
[ 'userlog',
'.admin.userlog.link', '.admin.userlog.text', [ 'canview:userlog', 'canview:*' ] ],
+ [ 'vgifts/',
+ '.admin.vgifts.link', '.admin.vgifts.text', [ 'siteadmin:vgifts', 'vgifts', sub {
+ return ( $LJ::IS_DEV_SERVER, LJ::Lang::ml( "/admin/index.tt.devserver" ) );
+ } ] ],
);
diff -r 4c7678ebddbf -r c4063e1602c6 cgi-bin/DW/VirtualGift.pm
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cgi-bin/DW/VirtualGift.pm Thu Oct 28 12:23:03 2010 +0800
@@ -0,0 +1,1165 @@
+#!/usr/bin/perl
+#
+# DW::VirtualGift - Provide virtual gifts for users
+#
+# Authors:
+# Jen Griffin <kareila@livejournal.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::VirtualGift;
+
+use strict;
+use warnings;
+
+use constant PROPLIST => qw/ vgiftid name created_t creatorid active
+ approved approved_by approved_why
+ custom featured description cost
+ mime_small mime_large /;
+# NOTE: remember to update &validate if you add new props
+
+use base 'LJ::MemCacheable';
+# MemCacheable methods ###################################
+ *_memcache_id = \&id; #
+ *_memcache_hashref_to_object = \&absorb_row; #
+sub _memcache_key_prefix { 'vgift_obj' } #
+sub _memcache_expires { 24*3600 } #
+sub _memcache_stored_props { return ( '1', PROPLIST ) } #
+# end MemCacheable methods ###############################
+
+use Digest::MD5 qw/ md5_hex /;
+use LJ::Event::VgiftApproved;
+
+
+# TABLE OF CONTENTS
+#
+# 1. Constructor methods (anything that modifies db values)
+# 2. Accessor methods (simple db reads and booleans)
+# 3. Memcache methods (for referencing & expiring keys)
+# 4. Validation methods (for checking user-supplied data)
+# 5. Aggregate methods (for mass lookups)
+# 6. End-user display methods (making things look purty)
+# 7. Notification methods (let people know about things)
+
+
+
+# 1. Constructor methods
+sub new {
+ my ( $class, $id ) = @_;
+ return undef if !$id || $id !~ /^\d+$/;
+ my $self = { vgiftid => $id };
+ bless $self, ( ref $class ? ref $class : $class );
+ return $self;
+}
+
+sub _init {
+ # This should only be called by the "create" method.
+ # It grabs an ID for the new vgift before calling the "new" method.
+ my ( $class, $err ) = @_;
+ if ( ref $class ) {
+ $$err = LJ::Lang::ml('vgift.error.init.reuse') if $err;
+ return undef;
+ }
+
+ my $vgiftid = LJ::alloc_global_counter('V');
+ unless ( $vgiftid ) {
+ $$err = LJ::Lang::ml('vgift.error.init.alloc') if $err;
+ return undef;
+ }
+
+ # we have an id, now initialize the object
+ return $class->new( $vgiftid );
+}
+
+sub create {
+ # opts are values for object properties as defined in PROPLIST.
+ # also allowed: 'error' which should be a scalar reference;
+ # 'img_small' & 'img_large' which should contain raw
+ # image data to be stored in MogileFS.
+ my ( $class, %opts ) = @_;
+ my %vg; # hash for storing row data
+ foreach ( PROPLIST ) {
+ $vg{$_} = $opts{$_} if defined $opts{$_};
+ # translate Perl nulls into MySQL nulls
+ $vg{$_} = undef if exists $vg{$_} && $vg{$_} eq '';
+ }
+ # don't allow created_t to be overridden
+ $vg{created_t} = time;
+
+ # enforce active/approved defaults for new gifts
+ if ( $vg{custom} && $vg{custom} eq 'Y' ) {
+ $vg{active} = 'Y';
+ $vg{approved} = 'N';
+ } else {
+ delete @vg{qw( active approved )};
+ }
+
+ # name is required
+ unless ( $vg{name} ) {
+ ${$opts{error}} = LJ::Lang::ml('vgift.error.create.noname');
+ return undef;
+ }
+
+ # name must be unique
+ my $dbr = LJ::get_db_reader();
+ my $exists = $dbr->selectrow_array( "SELECT name FROM vgift_ids " .
+ "WHERE name=?", undef, $vg{name} );
+ die $dbr->errstr if $dbr->err;
+
+ if ( $exists ) {
+ ${$opts{error}} = LJ::Lang::ml('vgift.error.create.samename');
+ return undef;
+ }
+ undef $dbr; # release handle
+
+ # creatorid defaults to the logged in user if there is one
+ $vg{creatorid} = LJ::get_remote() unless defined $vg{creatorid};
+ $vg{creatorid} = LJ::want_userid( $vg{creatorid} );
+
+ # validate input
+ return undef unless $class->validate_all( $opts{error}, \%vg );
+
+ # now that we're reasonably certain we have good data,
+ # grab an id and get to work
+ my $self = $class->_init( $opts{error} ) or return undef;
+ $vg{vgiftid} = $self->id;
+
+ # save pictures here, after getting id but before updating DB
+ return undef unless $self->_savepics( \%vg, %opts );
+
+ # construct SQL statement
+ my $dbh = LJ::get_db_writer();
+ my $props = join( ', ', keys %vg );
+ my $qs = join( ', ', map { '?' } keys %vg );
+ $dbh->do( "INSERT INTO vgift_ids ($props) VALUES ($qs)", undef, values %vg );
+ die $dbh->errstr if $dbh->err;
+
+ $self->_expire_aggregate_keys;
+ return $self->absorb_row( \%vg );
+}
+
+sub _savepic {
+ my ( $self, $size, $data ) = @_;
+ return undef unless $data && $self->id;
+
+ # img_mogkey checks $size, don't need to explicitly check here
+ return undef unless my $key = $self->img_mogkey( $size );
+
+ my %mime = ( JPG => 'image/jpeg',
+ GIF => 'image/gif',
+ PNG => 'image/png',
+ );
+ my ( undef, undef, $filetype ) = Image::Size::imgsize( $data );
+ return undef unless $mime{$filetype};
+
+ return undef unless my $mc = LJ::mogclient();
+ return undef unless $mc->store_content( $key, 'vgifts', $data );
+
+ return $mime{$filetype};
+}
+
+sub _savepics {
+ my ( $self, $ref, %opts ) = @_;
+ return undef unless $self->id;
+ return undef unless ref $ref eq 'HASH';
+
+ my $mime_small = $self->_savepic( 'small', $opts{img_small} );
+ if ( $opts{img_small} && ! $mime_small ) {
+ ${$opts{error}} = LJ::Lang::ml( 'vgift.error.savepics',
+ { size => 'small' } );
+ return undef;
+ }
+ my $mime_large = $self->_savepic( 'large', $opts{img_large} );
+ if ( $opts{img_large} && ! $mime_large ) {
+ ${$opts{error}} = LJ::Lang::ml( 'vgift.error.savepics',
+ { size => 'large' } );
+ return undef;
+ }
+ $ref->{mime_small} = $mime_small if $mime_small;
+ $ref->{mime_large} = $mime_large if $mime_large;
+
+ return 1;
+}
+
+sub edit {
+ # opts are values for object properties as defined in PROPLIST.
+ # also allowed: 'error' which should be a scalar reference;
+ # 'img_small' & 'img_large' which should contain raw
+ # image data to be stored in MogileFS.
+ my ( $self, %opts ) = @_;
+ return undef unless $self->id;
+
+ my %vg; # hash for storing row data
+ foreach ( PROPLIST ) {
+ $vg{$_} = $opts{$_} if defined $opts{$_};
+ # translate Perl nulls into MySQL nulls
+ $vg{$_} = undef if exists $vg{$_} && $vg{$_} eq '';
+ }
+ # don't allow created_t or vgiftid to be overridden
+ delete @vg{qw( created_t vgiftid )};
+
+ $vg{creatorid} = LJ::want_userid( $vg{creatorid} )
+ if defined $vg{creatorid};
+
+ # save pictures first
+ return undef unless $self->_savepics( \%vg, %opts );
+
+ return $self unless %vg; # no DB updates
+
+ # validate input
+ return undef unless $self->validate_all( $opts{error}, \%vg );
+
+ # expire aggregate keys based on current values
+ # (we expire again below, but that is based on the new values)
+ $self->_expire_aggregate_keys;
+
+ # construct SQL statement with new values
+ my $dbh = LJ::get_db_writer();
+ my @keys = keys %vg;
+ my $props = join( ', ', map { "$_=?" } @keys );
+ $dbh->do( "UPDATE vgift_ids SET $props WHERE vgiftid=?",
+ undef, values %vg, $self->id );
+ die $dbh->errstr if $dbh->err;
+
+ # update objects in memory
+ $self->{$_} = $vg{$_} foreach @keys;
+ $self->_remove_from_memcache; # LJ::MemCacheable
+ $self->_expire_relevant_keys( @keys );
+
+ return $self;
+}
+
+sub mark_active { $_[0]->edit( active => 'Y' ) }
+sub mark_inactive { $_[0]->edit( active => 'N' ) }
+
+sub tags {
+ # taglist is a comma separated string of tagnames.
+ # opts allowed: 'error' which should be a scalar reference;
+ # 'autovivify' boolean allowing new tags to be created in the DB.
+ # returns array of tagnames (or arrayref in scalar context)
+ my ( $self, $taglist, %opts ) = @_;
+ return undef unless my $id = $self->id;
+ return undef if $self->is_custom; # can't tag custom gifts
+ my $autovivify = $opts{autovivify};
+
+ my $error = sub {
+ ${$opts{error}} = shift if $opts{error};
+ return undef;
+ };
+
+ my $tagnames;
+
+ if ( defined $taglist ) { # save new tags
+ # taglist can be an arrayref or a comma separated string
+ my @newtags = ref $taglist eq 'ARRAY' ? @$taglist :
+ LJ::interest_string_to_list( $taglist );
+ # vgift tags are similar enough to interests that we can reuse code
+ unless ( @newtags ) { # just wipe existing tags and return
+ $self->_tagwipe;
+ return wantarray ? () : [];
+ }
+ my @valid_tags = LJ::validate_interest_list( @newtags );
+ my %invalid_tags = map { lc( $_ ) => 1 } @newtags;
+ delete @invalid_tags{@valid_tags};
+
+ if ( %invalid_tags ) {
+ return $error->( LJ::Lang::ml( 'vgift.error.tags.invalid', { taglist =>
+ $self->display_taglist( [ keys %invalid_tags ] ) } ) );
+ }
+
+ # this shouldn't be possible, but just in case...
+ return $error->( LJ::Lang::ml('vgift.error.tags.novalidtags') )
+ unless @valid_tags;
+
+ $tagnames = \@valid_tags; # save for later
+
+ # At this point we have the list of tag names to return,
+ # but we still need to store the tag ids in the database.
+
+ my $dbr = LJ::get_db_reader();
+ my $qs = join( ', ', map { '?' } @valid_tags );
+ my $dbdata = $dbr->selectall_arrayref( "SELECT keyword, kwid FROM sitekeywords " .
+ "WHERE keyword IN ($qs)", undef, @valid_tags );
+ die $dbr->errstr if $dbr->err;
+
+ my %dbtags = map { $_->[0] => $_->[1] } @$dbdata;
+ foreach my $tag ( @valid_tags ) {
+ next if $dbtags{$tag};
+ # try to create the tag if it didn't already exist
+ my $tagid = LJ::get_sitekeyword_id( $tag, $autovivify );
+ return $error->( LJ::Lang::ml( 'vgift.error.tags.create',
+ { tag => LJ::ehtml( $tag ) } ) ) unless $tagid;
+ $dbtags{$tag} = $tagid;
+ }
+
+ # delete previous tags & clear memcached tag data
+ $self->_tagwipe;
+
+ # construct SQL statement for adding new tags
+ my $dbh = LJ::get_db_writer();
+ my @tagids = values %dbtags;
+ my $qps = join( ', ', map { '(?,?)' } @tagids );
+ my @vals = map { ( $id, $_ ) } @tagids;
+ $dbh->do( "INSERT INTO vgift_tags (vgiftid, tagid) VALUES $qps", undef, @vals );
+ die $dbh->errstr if $dbh->err;
+
+ } else { # fetch existing tags
+ my $tags = LJ::MemCache::get( $self->taglist_memkey );
+ if ( $tags && ref $tags eq "ARRAY" ) {
+ return wantarray ? @$tags : $tags; # fast path out
+ }
+ my $dbr = LJ::get_db_reader();
+ $tagnames = $dbr->selectcol_arrayref(
+ "SELECT keyword FROM sitekeywords WHERE kwid IN " .
+ "(SELECT tagid FROM vgift_tags WHERE vgiftid=$id)"
+ );
+ die $dbr->errstr if $dbr->err;
+ }
+
+ LJ::MemCache::set( $self->taglist_memkey, $tagnames, 3600*24 );
+ return wantarray ? @$tagnames : $tagnames;
+}
+
+sub _tagwipe {
+ my ( $self, $tagid ) = @_;
+ return undef unless my $id = $self->id;
+
+ # Acts on a single gift. Remove $tagid from this gift,
+ # or if no $tagid specified, remove ALL tags from this gift.
+
+ my $dbh = LJ::get_db_writer();
+ if ( $tagid ) {
+ $dbh->do( "DELETE FROM vgift_tags WHERE vgiftid=$id AND tagid=?",
+ undef, $tagid );
+ } else {
+ $dbh->do( "DELETE FROM vgift_tags WHERE vgiftid=$id" );
+ }
+ die $dbh->errstr if $dbh->err;
+ $self->_expire_taglist_keys;
+ return 1;
+}
+
+sub remove_tag_by_id {
+ my ( $self, $tagid ) = @_;
+ return undef unless $tagid && $tagid =~ /^\d+$/;
+ return $self->_tagwipe( $tagid );
+}
+
+sub alter_tag {
+ my ( $self, $tagname, $newname ) = @_;
+
+ # For every gift that has $tagname, remove that tag.
+ # If $newname is provided, replace $tagname with $newname.
+
+ my $oldid = $self->get_tagid( $tagname );
+ return undef unless $oldid;
+
+ # We need to cache @vgs here before we do the SQL update,
+ # so that we remember which gifts we were acting on
+ # once the tags have been rewritten in the database.
+
+ my @vgs = $self->list_tagged_with( $tagname );
+ my $dbh = LJ::get_db_writer();
+
+ if ( $newname ) {
+ my $newid = $self->create_tag( $newname );
+ return undef unless $newid;
+
+ $dbh->do( "UPDATE vgift_tags SET tagid=$newid WHERE tagid=$oldid" );
+ die $dbh->errstr if $dbh->err;
+ $dbh->do( "UPDATE vgift_tagpriv SET tagid=$newid WHERE tagid=$oldid" );
+ die $dbh->errstr if $dbh->err;
+
+ } else {
+ $dbh->do( "DELETE FROM vgift_tags WHERE tagid=$oldid" );
+ die $dbh->errstr if $dbh->err;
+ $dbh->do( "DELETE FROM vgift_tagpriv WHERE tagid=$oldid" );
+ die $dbh->errstr if $dbh->err;
+ }
+
+ $self->_expire_taglist_keys( @vgs );
+ return 1;
+}
+
+sub create_tag {
+ my ( $self, $tagname ) = @_;
+ return LJ::get_sitekeyword_id( $tagname, 1 );
+}
+
+sub _addremove_tagpriv {
+ my ( $self, $sql, $tagname, $privname, $arg ) = @_;
+ return undef unless $sql && $tagname && $privname;
+ my $tagid = $self->get_tagid( $tagname ) or return undef;
+ my $prlid = $self->validate_priv( $privname ) or return undef;
+
+ my $dbh = LJ::get_db_writer();
+ $dbh->do( $sql, undef, $tagid, $prlid, $arg );
+ die $dbh->errstr if $dbh->err;
+ return 1;
+}
+
+sub add_priv_to_tag {
+ my $self = shift;
+ return $self->_addremove_tagpriv(
+ "INSERT IGNORE INTO vgift_tagpriv (tagid, prlid, arg)" .
+ " VALUES (?,?,?)", @_ );
+}
+
+sub remove_priv_from_tag {
+ my $self = shift;
+ return $self->_addremove_tagpriv(
+ "DELETE FROM vgift_tagpriv WHERE tagid=? AND prlid=?" .
+ " AND arg=?", @_ );
+}
+
+sub delete {
+ my ( $self, $u ) = @_;
+ return undef unless my $id = $self->id;
+ $u = $self->creator unless LJ::isu( $u );
+ return undef unless $self->can_be_deleted_by( $u );
+
+ # delete pictures from mogilefs
+ if ( $LJ::MOGILEFS_CONFIG{hosts} ) {
+ LJ::mogclient()->delete( $self->img_mogkey( 'large' ) );
+ LJ::mogclient()->delete( $self->img_mogkey( 'small' ) );
+ }
+
+ # wipe the relevant rows and memkeys
+ $self->_tagwipe;
+ $self->_expire_relevant_keys;
+ $self->_remove_from_memcache; # LJ::MemCacheable
+
+ my $dbh = LJ::get_db_writer();
+ $dbh->do( "DELETE FROM vgift_ids WHERE vgiftid=$id" );
+ die $dbh->errstr if $dbh->err;
+ return 1;
+}
+
+
+# 2. Accessor methods
+sub id { return $_[0]->{vgiftid} }
+*vgiftid = \&id;
+
+sub name { return $_[0]->_access( 'name' ) || ''; }
+sub name_ehtml { return LJ::ehtml( $_[0]->name ); }
+sub description { return $_[0]->_access( 'description' ) || ''; }
+sub description_ehtml { return LJ::ehtml( $_[0]->description ); }
+
+sub cost { return $_[0]->_access( 'cost' ) || 0 }
+sub active { return $_[0]->_access( 'active' ) || 'N' }
+sub custom { return $_[0]->_access( 'custom' ) || 'N' }
+sub featured { return $_[0]->_access( 'featured' ) || 'N' }
+sub creatorid { return $_[0]->_access( 'creatorid' ) || 0 }
+sub created_t { return $_[0]->_access( 'created_t' ) }
+
+sub creator { return LJ::load_userid( $_[0]->creatorid ) }
+sub is_inactive { return $_[0]->active eq 'N' ? 1 : 0 }
+sub is_active { return $_[0]->active eq 'Y' ? 1 : 0 }
+sub is_custom { return $_[0]->custom eq 'Y' ? 1 : 0 }
+sub is_featured { return $_[0]->featured eq 'Y' ? 1 : 0 }
+sub is_free { return $_[0]->cost ? 0 : 1 }
+
+sub approved { return $_[0]->_access( 'approved' ) || '' }
+sub approved_by { return $_[0]->_access( 'approved_by' ) || '' }
+sub approved_why { return $_[0]->_access( 'approved_why' ) || '' }
+sub is_approved { return $_[0]->approved eq 'Y' ? 1 : 0 }
+sub is_rejected { return $_[0]->approved eq 'N' ? 1 : 0 }
+sub is_queued { return $_[0]->approved ? 0 : 1 }
+sub approver { return LJ::load_userid( $_[0]->approved_by ) }
+
+sub img_small { return $_[0]->_loadpic( 'small' ) }
+sub img_large { return $_[0]->_loadpic( 'large' ) }
+sub img_small_html { return $_[0]->_loadpic_html( 'small' ) }
+sub img_large_html { return $_[0]->_loadpic_html( 'large' ) }
+sub mime_small { return $_[0]->_access( 'mime_small' ) }
+sub mime_large { return $_[0]->_access( 'mime_large' ) }
+sub mime_type {
+ my ( $self, $size ) = @_;
+ return undef unless $size;
+ return $self->mime_small if $size eq 'small';
+ return $self->mime_large if $size eq 'large';
+ return undef; # invalid size
+}
+
+sub _access {
+ my ( $self, $prop ) = @_;
+ return undef unless $prop = lc $prop;
+ return $self->{$prop} if defined $self->{$prop};
+ $self->_load;
+ return $self->{$prop};
+}
+
+sub _load {
+ my $self = shift;
+ return undef unless $self->id;
+
+ return $self if $self->{_loaded}; # from absorb_row
+ return $self if $self->_load_from_memcache; # LJ::MemCacheable
+
+ # find row in database
+ my $dbr = LJ::get_db_reader();
+ my $props = join( ', ', PROPLIST );
+
+ my $row = $dbr->selectrow_hashref(
+ "SELECT $props FROM vgift_ids WHERE vgiftid=?",
+ undef, $self->id );
+ die $dbr->errstr if $dbr->err;
+ return undef unless $row;
+
+ # store retrieved data
+ $self->absorb_row( $row );
+ $self->_store_to_memcache; # LJ::MemCacheable
+
+ return $self;
+}
+
+sub absorb_row {
+ my ( $self, $row ) = @_;
+ return undef unless $row;
+
+ $self->{$_} = $row->{$_} foreach PROPLIST;
+ $self->{_loaded} = 1;
+ return $self;
+}
+
+sub _loadpic {
+ my ( $self, $size ) = @_;
+
+ return undef unless my $id = $self->id;
+ return undef unless $self->mime_type( $size );
+
+ return "$LJ::SITEROOT/vgift/$id/$size";
+}
+
+sub _loadpic_html {
+ my ( $self, $size ) = @_;
+
+ return '' unless $size && $size =~ /^(small|large)$/;
+ return '' unless $self->id;
+ my $url = $self->_loadpic( $size );
+ return LJ::Lang::ml( 'vgift.error.loadpic', { size => $size } )
+ unless $url;
+
+ my $name = $self->name_ehtml;
+ my $desc = $self->description_ehtml;
+ return "<img alt='$desc' title='$name' src='$url' />";
+}
+
+sub img_mogkey {
+ my ( $self, $size ) = @_;
+ return undef unless $size && $size =~ /^(small|large)$/;
+ return undef unless my $id = $self->id;
+ return "vgift_img_$size:$id";
+}
+
+# tagnames and interests are both in sitekeywords
+sub get_tagname { return LJ::get_interest( $_[1] ) }
+
+sub get_tagid { return LJ::get_sitekeyword_id( $_[1], 0 ) }
+
+sub can_be_approved_by {
+ my ( $self, $u ) = @_;
+ $u = LJ::want_user( $u ) or return undef;
+
+ # creators can't approve their own gifts
+ return 0 if $u->equals( $self->creator );
+
+ # otherwise, same privileges as for edits
+ return $self->can_be_edited_by( $u );
+}
+
+sub can_be_edited_by {
+ my ( $self, $u ) = @_;
+
+ # don't allow editing of gifts that are active in the shop
+ return 0 if $self->is_active;
+
+ $u = LJ::want_user( $u ) or return undef;
+ # creators can edit their own inactive gifts
+ return 1 if $u->equals( $self->creator );
+ # siteadmins can edit any inactive gift
+ return 1 if $u->has_priv( 'siteadmin', 'vgifts' );
+
+ return 0;
+}
+
+sub can_be_deleted_by {
+ my $self = shift;
+
+ # FIXME: if the vgift has been purchased, don't allow
+
+ # otherwise, same privileges as for edits
+ return $self->can_be_edited_by( @_ );
+}
+
+sub checksum {
+ my ( $self ) = @_;
+ return unless $self->_load;
+
+ # generate a checksum based on attribute values
+ my @attrvals;
+ foreach my $prop ( PROPLIST ) {
+ push @attrvals, $self->{$prop} || 'NULL';
+ }
+
+ if ( my $mc = LJ::mogclient() ) {
+ foreach my $size ( qw( large small ) ) {
+ my $data = $mc->get_file_data( $self->img_mogkey( $size ) );
+ push @attrvals, $data ? $$data : 'NULL';
+ }
+ }
+
+ return md5_hex( join ' ', @attrvals );
+}
+
+sub created_ago_text {
+ my ( $self ) = @_;
+ return '' unless $self->id;
+ return LJ::diff_ago_text( $self->created_t );
+}
+
+sub is_untagged {
+ my ( $self ) = @_;
+ my $id = $self->id or return undef;
+ foreach ( $self->list_untagged ) {
+ return 1 if $id == $_->id;
+ }
+ return 0; # not in the untagged list
+}
+
+
+# 3. Memcache methods
+sub _expire_relevant_keys {
+ # this is called from delete/edit to expire specific keys
+ # relevant to the particular object being acted on.
+ my ( $self, @props ) = @_;
+ return undef unless $self->id;
+ @props = PROPLIST unless @props;
+ my %prop = map { $_ => 1 } @props;
+
+ if ( $prop{mime_small} ) {
+ # expire memcache for img_small (set in Apache::LiveJournal)
+ LJ::MemCache::delete( $self->img_memkey( 'small' ) );
+ }
+
+ if ( $prop{mime_large} ) {
+ # expire memcache for img_large (set in Apache::LiveJournal)
+ LJ::MemCache::delete( $self->img_memkey( 'large' ) );
+ }
+
+ return $self->_expire_aggregate_keys( @props );
+}
+
+sub _expire_aggregate_keys {
+ # this is called from create/edit to expire aggregate keys
+ # based on specified values, or from _expire_relevant_keys.
+ my ( $self, @props ) = @_;
+ return undef unless $self->id;
+ @props = PROPLIST unless @props;
+ my %prop = map { $_ => 1 } @props;
+
+ if ( $prop{creatorid} ) {
+ # expire memcache for list_created_by
+ LJ::MemCache::delete( $self->created_by_memkey );
+ }
+
+ if ( $prop{creatorid} || $prop{active} || $prop{approved} ) {
+ # expire memcache for fetch_creatorcounts
+ LJ::MemCache::delete( $self->creatorcounts_memkey );
+ }
+
+ return $self;
+}
+
+sub _expire_taglist_keys {
+ # this is called from _tagwipe to expire tag-related keys.
+ my ( $self, @vgs ) = @_; # may pass in additional vgift objects
+ @vgs = ( $self ) unless @vgs;
+ foreach ( @vgs ) {
+ next unless $_->id;
+ # expire memcache for vgift list of tags
+ LJ::MemCache::delete( $_->taglist_memkey );
+ }
+ # the rest are aggregate and only need to be expired once
+
+ # expire memcache for fetch_tagcounts methods
+ LJ::MemCache::delete( $self->tagcounts_approved_memkey );
+ LJ::MemCache::delete( $self->tagcounts_active_memkey );
+ # expire memcache for list_untagged
+ LJ::MemCache::delete( $self->untagged_memkey );
+ # expire memcache for list_nonpriv_tags
+ LJ::MemCache::delete( $self->nonpriv_tags_memkey );
+ # we can't force expiry of all individual user taglists
+ # or tagid lists - these are uncached every few minutes
+
+ return $self;
+}
+
+sub img_memkey {
+ my ( $self, $size ) = @_;
+ return undef unless $size && $size =~ /^(small|large)$/;
+ return undef unless my $id = $self->id;
+ return [$id, "mogp.vg.$size.$id"];
+}
+
+sub created_by_memkey {
+ my ( $self, $uid ) = @_;
+ $uid = $self->creatorid unless defined $uid;
+ return [$uid, "vgift.creatorid.$uid"];
+}
+
+sub taglist_memkey {
+ my ( $self ) = @_;
+ return undef unless my $id = $self->id;
+ return [$id, "vgift.taglist.$id"]; # list of tags for this giftid
+}
+
+sub tagged_with_memkey {
+ my ( $self, $tagid ) = @_;
+ return undef unless defined $tagid;
+ return [$tagid, "vgift.tagid.$tagid"]; # list of gifts for this tagid
+}
+
+sub untagged_memkey { return 'vgift_untagged'; }
+
+sub tagcounts_approved_memkey { return 'vgift_tagcounts_approved'; }
+
+sub tagcounts_active_memkey { return 'vgift_tagcounts_active'; }
+
+sub creatorcounts_memkey { return 'vgift_creatorcounts'; }
+
+sub nonpriv_tags_memkey { return 'vgift_nonpriv_tags'; }
+
+
+# 4. Validation methods
+sub validate_all {
+ my ( $self, $err, $arg ) = @_;
+ # err is optional scalar reference for error message.
+ # arg is optional hashref; if missing, validate the object.
+ my $data = $arg || $self;
+ my $ok = 1;
+ $ok &&= $self->validate( $_ => $data->{$_}, $err ) foreach PROPLIST;
+ return $ok;
+}
+
+sub validate {
+ my ( $self, $key, $val, $err ) = @_;
+
+ return $self->_valid_mime( $val, $err, $key ) if $key eq 'mime_small';
+ return $self->_valid_mime( $val, $err, $key ) if $key eq 'mime_large';
+ return $self->_valid_text( $val, $err, $key ) if $key eq 'description';
+ return $self->_valid_text( $val, $err, $key ) if $key eq 'approved_why';
+ return $self->_valid_name( $val, $err, $key ) if $key eq 'name';
+ return $self->_valid_y_n( $val, $err, $key ) if $key eq 'active';
+ return $self->_valid_y_n( $val, $err, $key ) if $key eq 'custom';
+ return $self->_valid_y_n( $val, $err, $key ) if $key eq 'featured';
+ return $self->_valid_y_n( $val, $err, $key ) if $key eq 'approved';
+ return $self->_valid_uid( $val, $err, $key ) if $key eq 'approved_by';
+ return $self->_valid_uid( $val, $err, $key ) if $key eq 'creatorid';
+ return $self->_valid_int( $val, $err, $key ) if $key eq 'vgiftid';
+ return $self->_valid_int( $val, $err, $key ) if $key eq 'created_t';
+ return $self->_valid_int( $val, $err, $key ) if $key eq 'cost';
+
+ # default case if no test defined for $key: assume invalid
+ $$err = LJ::Lang::ml( 'vgift.error.validate.property', { key => $key } );
+ return 0;
+}
+
+sub _valid_name {
+ my ( $self, $name, $err ) = @_;
+
+ return 1 unless $name;
+
+ if ( $name !~ /\S/ || $name =~ /[\r\n\t\0]/ ) {
+ $$err = LJ::Lang::ml('vgift.error.validate.name');
+ return 0;
+ }
+ return $self->_valid_text( $name, $err, 'name' );
+}
+
+sub _valid_mime {
+ my ( $self, $mime, $err, $prop ) = @_;
+
+ if ( $mime && $mime !~ /^image\// ) {
+ $$err = LJ::Lang::ml( 'vgift.error.validate.value', { prop => $prop } );
+ return 0;
+ }
+ return 1;
+}
+
+sub _valid_int {
+ my ( $self, $int, $err, $prop ) = @_;
+
+ if ( $int && $int !~ /^\d+$/ ) {
+ $$err = LJ::Lang::ml( 'vgift.error.validate.value', { prop => $prop } );
+ return 0;
+ }
+ return 1;
+}
+
+sub _valid_y_n {
+ my ( $self, $yn, $err, $prop ) = @_;
+
+ if ( $yn && $yn !~ /^[YN]$/i ) {
+ $$err = LJ::Lang::ml( 'vgift.error.validate.value', { prop => $prop } );
+ return 0;
+ }
+ return 1;
+}
+
+sub _valid_uid {
+ my ( $self, $uid, $err, $prop ) = @_;
+
+ if ( defined $uid && ! LJ::load_userid( $uid ) ) {
+ $$err = LJ::Lang::ml( 'vgift.error.validate.value', { prop => $prop } );
+ return 0;
+ }
+ # Not going to check user privileges at this level.
+ # Also, uid 0 ought to be valid (indicates created by "the site").
+ return 1;
+}
+
+sub _valid_text {
+ my ( $self, $text, $err, $prop ) = @_;
+
+ unless ( LJ::text_in( $text ) ) {
+ $$err = LJ::Lang::ml( 'vgift.error.validate.text', { prop => $prop } );
+ return 0;
+ }
+ return 1;
+}
+
+sub validate_priv {
+ my ( $self, $priv ) = @_;
+ return undef unless $priv;
+ my $dbr = LJ::get_db_reader();
+ if ( $priv =~ /^\d+$/ ) {
+ # id->name
+ return $dbr->selectrow_array(
+ 'SELECT privcode FROM priv_list WHERE prlid = ?',
+ undef, $priv )
+ } else {
+ # name->id
+ return $dbr->selectrow_array(
+ 'SELECT prlid FROM priv_list WHERE privcode = ?',
+ undef, $priv )
+ }
+}
+
+
+# 5. Aggregate methods
+sub _findall {
+ my ( $self, $sql ) = @_;
+ return undef unless $sql;
+ my $dbr = LJ::get_db_reader();
+ my $ids = $dbr->selectcol_arrayref(
+ "SELECT vgiftid FROM vgift_ids WHERE $sql ORDER BY created_t DESC" );
+ die $dbr->errstr if $dbr->err;
+ return undef unless $ids;
+ return map { $self->new( $_ ) } @$ids;
+}
+
+sub _findall_cached {
+ my ( $self, $memkey, $sql ) = @_;
+ return undef unless $sql;
+ return $self->_findall( $sql ) unless $memkey;
+
+ my $data = LJ::MemCache::get( $memkey );
+ return map { $self->new( $_ ) } @$data if $data && ref $data;
+
+ # if it's not in memcache, run the query and update memcache
+ my @vgs = $self->_findall( $sql );
+ my @ids = map { $_->id } @vgs;
+ LJ::MemCache::set( $memkey, \@ids, 24*3600 );
+ return @vgs;
+}
+
+sub list_inactive { $_[0]->_findall( "active='N'" ) }
+
+sub list_queued { $_[0]->_findall( "approved IS NULL AND custom='N'" ) }
+
+sub list_recent {
+ my ( $self, $days ) = @_;
+ return undef unless defined $days && $days =~ /^\d+$/;
+ my $secs = time - $days * 24 * 3600;
+ return $self->_findall( "created_t > $secs AND custom='N'" );
+}
+
+sub list_created_by {
+ my ( $self, $u ) = @_;
+ my $uid = LJ::want_userid( $u );
+ return undef unless $uid;
+ my $memkey = $self->created_by_memkey( $uid );
+ return $self->_findall_cached( $memkey, "creatorid=$uid AND custom='N'" );
+}
+
+sub list_untagged {
+ my ( $self ) = @_;
+ return $self->_findall_cached( $self->untagged_memkey,
+ "custom='N' AND vgiftid NOT IN (SELECT DISTINCT vgiftid FROM vgift_tags)" );
+}
+
+sub list_tagged_with {
+ my ( $self, $tagname ) = @_;
+ return undef if !$tagname || ref $tagname;
+
+ my $tagid = $self->get_tagid( $tagname );
+ return undef unless $tagid;
+
+ my $memkey = $self->tagged_with_memkey( $tagid );
+ my $vgs = LJ::MemCache::get( $memkey ) || [];
+
+ unless ( @$vgs ) {
+ my $dbr = LJ::get_db_reader();
+ $vgs = $dbr->selectcol_arrayref( "SELECT vgiftid FROM vgift_tags " .
+ "WHERE tagid=$tagid " .
+ "ORDER BY vgiftid DESC" );
+ die $dbr->errstr if $dbr->err;
+ LJ::MemCache::set( $memkey, $vgs, 600 );
+ }
+
+ return map { $self->new( $_ ) } @$vgs;
+}
+
+sub _fetch_tagcounts {
+ my ( $self, $memkey, $select ) = @_;
+ my $counts = LJ::MemCache::get( $memkey ) || {};
+
+ unless ( %$counts ) {
+ my $dbr = LJ::get_db_reader();
+ my $rows = $dbr->selectall_arrayref(
+ "SELECT sk.keyword, COUNT(vt.vgiftid) " .
+ "FROM sitekeywords AS sk, vgift_tags AS vt, vgift_ids AS vi " .
+ "WHERE sk.kwid = vt.tagid AND vi.vgiftid = vt.vgiftid " .
+ "AND vi.$select GROUP BY keyword ORDER BY keyword ASC" );
+ die $dbr->errstr if $dbr->err;
+
+ $counts = { map { $_->[0] => $_->[1] } @$rows };
+
+ # also select from vgift_tagpriv in case we've defined
+ # a privileged tag with no gifts available
+
+ my $privempty = $dbr->selectcol_arrayref(
+ "SELECT keyword FROM sitekeywords WHERE kwid IN " .
+ "(SELECT DISTINCT tagid FROM vgift_tagpriv WHERE tagid NOT IN ".
+ "(SELECT DISTINCT tagid FROM vgift_tags)) ORDER BY keyword ASC" );
+ die $dbr->errstr if $dbr->err;
+
+ $counts->{$_} = 0 foreach @$privempty;
+
+ LJ::MemCache::set( $memkey, $counts, 24*3600 );
+ }
+
+ return $counts;
+}
+
+sub fetch_tagcounts_approved {
+ my ( $self ) = @_;
+ return $self->_fetch_tagcounts( $self->tagcounts_approved_memkey,
+ "approved='Y'" );
+}
+
+sub fetch_tagcounts_active {
+ my ( $self ) = @_;
+ return $self->_fetch_tagcounts( $self->tagcounts_active_memkey,
+ "active='Y'" );
+}
+
+sub fetch_creatorcounts {
+ my ( $self, $type ) = @_;
+ my $memkey = $self->creatorcounts_memkey;
+ my $counts = LJ::MemCache::get( $memkey ) || {};
+
+ unless ( %$counts ) {
+ my $dbr = LJ::get_db_reader();
+ my $ids = $dbr->selectcol_arrayref(
+ "SELECT DISTINCT creatorid FROM vgift_ids WHERE custom='N'" );
+ die $dbr->errstr if $dbr->err;
+
+ foreach my $uid ( @$ids ) {
+ $counts->{$uid}->{active} = 0;
+ $counts->{$uid}->{approved} = 0;
+
+ foreach my $vg ( $self->list_created_by( $uid ) ) {
+ $counts->{$uid}->{active}++ if $vg->is_active;
+ $counts->{$uid}->{approved}++ if $vg->is_approved;
+ }
+ }
+
+ LJ::MemCache::set( $memkey, $counts, 24*3600 );
+ }
+
+ return { map { $_ => $counts->{$_}->{$type} } keys %$counts }
+ if $type && $type =~ /^(active|approved)$/;
+ return $counts;
+}
+
+sub list_nonpriv_tags {
+ my ( $self ) = @_;
+ my $memkey = $self->nonpriv_tags_memkey;
+ my $names = LJ::MemCache::get( $memkey ) || [];
+
+ unless ( @$names ) {
+ my $dbr = LJ::get_db_reader();
+ $names = $dbr->selectcol_arrayref(
+ "SELECT keyword FROM sitekeywords WHERE kwid IN " .
+ "(SELECT DISTINCT tagid FROM vgift_tags WHERE tagid NOT IN ".
+ "(SELECT DISTINCT tagid FROM vgift_tagpriv)) ORDER BY keyword ASC"
+ );
+ die $dbr->errstr if $dbr->err;
+
+ LJ::MemCache::set( $memkey, $names, 24*3600 );
+ }
+ return wantarray ? @$names : $names;
+}
+
+sub list_tagprivs {
+ my ( $self, $tagname ) = @_;
+ return undef if !$tagname || ref $tagname;
+
+ my $tagid = $self->get_tagid( $tagname );
+ return undef unless $tagid;
+
+ my $dbr = LJ::get_db_reader();
+ my $rows = $dbr->selectall_arrayref( "SELECT pl.privcode, tp.arg FROM "
+ . "priv_list AS pl, vgift_tagpriv AS tp "
+ . "WHERE tp.tagid=$tagid AND "
+ . "pl.prlid=tp.prlid "
+ . "ORDER BY privcode ASC, arg ASC" );
+ die $dbr->errstr if $dbr->err;
+
+ return @$rows;
+}
+
+
+# 6. End-user display methods
+sub display_basic {
+ my $self = shift;
+ my $id = $self->id or return undef;
+ my $ret = '';
+
+ $ret .= "<h2>" . $self->name_ehtml . " (#" . $self->id . ")</h2><p><b>";
+ $ret .= LJ::Lang::ml( 'vgift.display.createdby',
+ { user => $self->creator->ljuser_display,
+ ago => $self->created_ago_text } );
+ $ret .= "</b></p>\n" . $self->img_small_html;
+ $ret .= "<p>" . $self->description_ehtml . "</p>\n";
+ $ret .= "<p><b>" . LJ::Lang::ml( 'vgift.display.label.tags' ) . "</b> ";
+ $ret .= $self->display_taglist . "</p>\n";
+
+ return $ret;
+}
+
+sub display_summary {
+ my $self = shift;
+ my $id = $self->id or return undef;
+ my $ret = '';
+
+ $ret .= "<div style='clear: left'></div>\n";
+ $ret .= "<div style='float: left; margin-right: 2em'>";
+ $ret .= $self->img_small_html;
+ $ret .= "</div><p><b>";
+ $ret .= $self->name_ehtml . '</b>: <em>' . $self->description_ehtml;
+ $ret .= "</em><br />";
+ $ret .= LJ::Lang::ml( 'vgift.display.createdby',
+ { user => $self->creator->ljuser_display,
+ ago => $self->created_ago_text } );
+ $ret .= "<br />" . LJ::Lang::ml( 'vgift.display.label.cost' ) . " ";
+ $ret .= $self->display_cost . "</p>";
+
+ return $ret;
+}
+
+sub display_taglist {
+ # reverse of tags method: take in arrayref, return string
+ my ( $self, $tags ) = @_;
+ $tags = $self->tags unless $tags && ref $tags eq 'ARRAY';
+ return LJ::ehtml( join( ', ', sort { $a cmp $b } @$tags ) );
+}
+
+sub display_cost {
+ my ( $self, $cost ) = @_;
+ $cost ||= $self->cost unless $self->is_free;
+ return $cost ? LJ::Lang::ml( 'vgift.display.cost.points', { cost => $cost } )
+ : LJ::Lang::ml( 'vgift.display.cost.free' );
+}
+
+sub display_vieweditlinks {
+ my ( $self, $review ) = @_;
+ my $id = $self->id or return '';
+ my $linkroot = "$LJ::SITEROOT/admin/vgifts/";
+ my %modes = ( view => LJ::Lang::ml('vgift.display.linktext.viewedit'),
+ review => LJ::Lang::ml('vgift.display.linktext.review'),
+ delete => LJ::Lang::ml('vgift.display.linktext.delete'),
+ );
+ delete $modes{review} unless $review;
+ delete $modes{delete} if $self->is_active;
+
+ my $text = "";
+ foreach my $mode qw( view review delete ) {
+ next unless $modes{$mode};
+ $text .= ' | ' if $text;
+ $text .= "<a href='$linkroot?mode=$mode&id=$id'>$modes{$mode}</a>";
+ }
+ return $text;
+}
+
+sub display_viewbylink {
+ my ( $self, $uid ) = @_;
+ my $u = LJ::want_user( $uid ) or return '';
+ my $user = $u->user;
+ my $linkroot = "$LJ::SITEROOT/admin/vgifts/";
+ return " <a href='$linkroot?mode=view&user=$user'>"
+ . LJ::Lang::ml('vgift.display.linktext.viewgifts') . "</a>";
+}
+
+sub display_creatorlist {
+ my ( $self, $num ) = @_;
+ my $data = $self->fetch_creatorcounts;
+ my $users = LJ::load_userids( keys %$data );
+ my $sort = sub {
+ $data->{$b}->{active} <=> $data->{$a}->{active} ||
+ $data->{$b}->{approved} <=> $data->{$a}->{approved} ||
+ $users->{$a}->user cmp $users->{$b}->user };
+ my @creatorlist = map { [ $users->{$_}, $data->{$_}->{approved},
+ $data->{$_}->{active} ] } sort $sort keys %$data;
+ my @printlist;
+
+ foreach ( @creatorlist ) {
+ last if $num && $num == scalar @printlist;
+ my ( $u, $approved, $active ) = @$_;
+ my $text = '<li>';
+ $text .= LJ::Lang::ml( 'vgift.display.creatorlist.counts',
+ { user => $u->ljuser_display,
+ approved => $approved,
+ active => $active } );
+ $text .= $self->display_viewbylink( $u ) . "</li>\n";
+ push @printlist, $text;
+ }
+ return join '', @printlist;
+}
+
+
+# 7. Notification methods
+sub notify_approved {
+ my ( $self, $id ) = @_;
+
+ if ( $id ) { # transform class method -> object method
+ $self = $self->new( $id ) or return;
+ } else { # verify object
+ $id = $self->id or return;
+ }
+ # make sure the gift was actually reviewed
+ return if $self->is_queued;
+
+ # notify the user (inbox only, no opt-out)
+ my @args = ( $self->creator, $self->approver, $self );
+ LJ::Event::VgiftApproved->new( @args )->fire;
+}
+
+
+1;
diff -r 4c7678ebddbf -r c4063e1602c6 cgi-bin/LJ/Event/VgiftApproved.pm
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cgi-bin/LJ/Event/VgiftApproved.pm Thu Oct 28 12:23:03 2010 +0800
@@ -0,0 +1,149 @@
+#!/usr/bin/perl
+#
+# LJ::Event::VgiftApproved
+#
+# Event for approving a virtual gift
+#
+# Authors:
+# Jen Griffin <kareila@livejournal.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 LJ::Event::VgiftApproved;
+use strict;
+use base 'LJ::Event';
+use Carp qw(croak);
+use DW::VirtualGift;
+
+sub new {
+ my ( $class, $u, $fromu, $vgift ) = @_;
+
+ croak 'Not to an LJ::User' unless LJ::isu( $u );
+ croak 'Not from an LJ::User' unless LJ::isu( $fromu );
+
+ $vgift = DW::VirtualGift->new( $vgift ) unless ref $vgift;
+ croak 'Invalid vgift' unless $vgift && $vgift->name;
+
+ return $class->SUPER::new( $u, $fromu->id, $vgift->id );
+}
+
+# access args
+sub fromuid { return $_[0]->arg1 }
+
+sub vgiftid { return $_[0]->arg2 }
+
+sub fromu {
+ my ( $self ) = @_;
+ $self->{fromu} = LJ::load_userid( $self->fromuid )
+ unless $self->{fromu};
+ return $self->{fromu};
+}
+
+sub vgift {
+ my ( $self ) = @_;
+ $self->{vgift} = DW::VirtualGift->new( $self->vgiftid )
+ unless $self->{vgift};
+ return $self->{vgift};
+}
+
+# message content
+sub _summary {
+ my ( $self, $admin ) = @_;
+ return BML::ml( 'event.vgift.notfound' )
+ unless $self->vgift && $self->vgift->name;
+ my $yn = $self->vgift->approved;
+ # event.vgift.approved.content.Y = thumbs up
+ # event.vgift.approved.content.N = thumbs down
+ return BML::ml( "event.vgift.approved.content.$yn",
+ { vgift => $self->vgift->name_ehtml,
+ admin => $admin } );
+}
+
+sub as_string { return $_[0]->_summary( $_[0]->fromu->display_username ) }
+
+sub as_html { return $_[0]->_summary( $_[0]->fromu->ljuser_display ) }
+
+sub as_html_actions {
+ my ( $self ) = @_;
+ my $url = "$LJ::SITEROOT/admin/vgifts/?mode=view&id=" . $self->vgiftid;
+ my $ret = "<div class='actions'>";
+ $ret .= BML::ml( 'event.vgift.approved.actions',
+ { aopts => "href='$url'" } );
+ $ret .= "</div>\n";
+
+ return $ret;
+}
+
+sub content_summary { return $_[0]->as_html }
+
+sub content {
+ my ( $self ) = @_;
+ return BML::ml( 'event.vgift.notfound' )
+ unless $self->vgift && $self->vgift->name;
+ my $yn = $self->vgift->approved;
+ my $ret = '<p>';
+ $ret .= BML::ml( "event.vgift.approved.msg.$yn",
+ { vgift => $self->vgift->name_ehtml } ) . '</p>';
+ if ( $self->vgift && $self->vgift->approved_why ) {
+ my $reason = LJ::ehtml( $self->vgift->approved_why );
+ my $mltext = BML::ml( 'event.vgift.approved.reason',
+ { admin => $self->fromu->ljuser_display } );
+ $ret .= "<p>$mltext</p><p><q>$reason</q></p>\n";
+ }
+ $ret .= $self->as_html_actions;
+
+ return $ret;
+}
+
+# subscriptions are always on, can't be turned off
+sub is_common { 1 }
+
+sub is_visible { 0 }
+
+sub always_checked { 1 }
+
+# override parent class subscription methods to always return
+# a subscription object for the user - copied from LJ::Event::XPostSuccess
+sub subscriptions {
+ my ( $self, %args ) = @_;
+ my $cid = delete $args{'cluster'}; # optional
+ my $limit = delete $args{'limit'}; # optional
+ croak("Unknown options: " . join(', ', keys %args)) if %args;
+ croak("Can't call in web context") if LJ::is_web_context();
+
+ my @subs;
+ my $u = $self->u;
+ return unless $cid == $u->clusterid;
+
+ my $row = { userid => $self->u->id,
+ ntypeid => LJ::NotificationMethod::Inbox->ntypeid, # Inbox
+ };
+
+ push @subs, LJ::Subscription->new_from_row($row);
+
+ push @subs, eval { $self->SUPER::subscriptions(cluster => $cid,
+ limit => $limit) };
+
+ return @subs;
+}
+
+sub get_subscriptions {
+ my ( $self, $u, $subid ) = @_;
+
+ unless ($subid) {
+ my $row = { userid => $u->{userid},
+ ntypeid => LJ::NotificationMethod::Inbox->ntypeid, # Inbox
+ };
+
+ return LJ::Subscription->new_from_row($row);
+ }
+
+ return $self->SUPER::get_subscriptions($u, $subid);
+}
+
+
+1;
diff -r 4c7678ebddbf -r c4063e1602c6 cgi-bin/ljdefaults.pl
--- a/cgi-bin/ljdefaults.pl Thu Oct 28 11:15:07 2010 +0800
+++ b/cgi-bin/ljdefaults.pl Thu Oct 28 12:23:03 2010 +0800
@@ -186,6 +186,7 @@ no strict "vars";
$MOGILEFS_CONFIG{classes} ||= {};
$MOGILEFS_CONFIG{classes}->{temp} ||= 2;
$MOGILEFS_CONFIG{classes}->{userpics} ||= 3;
+ $MOGILEFS_CONFIG{classes}->{vgifts} ||= 3;
# Default to allow all reproxying.
%REPROXY_DISABLE = () unless %REPROXY_DISABLE;
@@ -220,6 +221,7 @@ no strict "vars";
NewUserpic
PollVote
UserExpunged
+ VgiftApproved
);
foreach my $evt (@LJ::EVENT_TYPES) {
$evt = "LJ::Event::$evt";
diff -r 4c7678ebddbf -r c4063e1602c6 cgi-bin/ljlib.pl
--- a/cgi-bin/ljlib.pl Thu Oct 28 11:15:07 2010 +0800
+++ b/cgi-bin/ljlib.pl Thu Oct 28 12:23:03 2010 +0800
@@ -2019,7 +2019,7 @@ sub get_secret
#
# LJ-generic domains:
# $dom: 'S' == style, 'P' == userpic, 'A' == stock support answer
-# 'E' == external user,
+# 'E' == external user, 'V' == vgifts,
# 'L' == poLL, 'M' == Messaging, 'H' == sHopping cart,
# 'F' == PubSubHubbub subscription id (F for Fred),
# 'K' == sitekeyword, 'I' == shopping cart Item
@@ -2032,7 +2032,7 @@ sub alloc_global_counter
# $dom can come as a direct argument or as a string to be mapped via hook
my $dom_unmod = $dom;
- unless ( $dom =~ /^[ESLPAHCMFKI]$/ ) {
+ unless ( $dom =~ /^[ESLPAHCMFKIV]$/ ) {
$dom = LJ::Hooks::run_hook('map_global_counter_domain', $dom);
}
return LJ::errobj("InvalidParameters", params => { dom => $dom_unmod })->cond_throw
@@ -2073,6 +2073,8 @@ sub alloc_global_counter
$newmax = $dbh->selectrow_array( "SELECT MAX(pollid) FROM pollowner" );
} elsif ( $dom eq 'F' ) {
$newmax = $dbh->selectrow_array( 'SELECT MAX(id) FROM syndicated_hubbub2' );
+ } elsif ( $dom eq 'V' ) {
+ $newmax = $dbh->selectrow_array( "SELECT MAX(vgiftid) FROM vgift_ids" );
} elsif ( $dom eq 'K' ) {
# pick maximum id from sitekeywords & interests
my $max_sitekeys = $dbh->selectrow_array( "SELECT MAX(kwid) FROM sitekeywords" );
diff -r 4c7678ebddbf -r c4063e1602c6 htdocs/admin/vgifts/inactive.bml
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/admin/vgifts/inactive.bml Thu Oct 28 12:23:03 2010 +0800
@@ -0,0 +1,302 @@
+<?_code
+{
+ use strict;
+ use vars qw($title $body $page $action %GET %POST);
+
+ use DW::VirtualGift;
+
+ $title = $ML{'.title'};
+ $body = "";
+ $page = "$LJ::SITEROOT/admin/vgifts/inactive";
+ $action = 'inactive'; # for $postform
+
+ # helper routines
+
+ my $error = sub {
+ $title = $ML{'.error'};
+ $body = join '', @_;
+ return undef;
+ };
+ my $errmsg;
+
+ my $loose_refer = sub {
+ return 1 unless my $refer = BML::get_client_header('Referer');
+ my ( $getargs ) = ( $refer =~ m/\?(.*)$/ );
+ return LJ::check_referer( $page ) unless $getargs;
+ return LJ::check_referer( "$page?$getargs" );
+ };
+
+ my $strict_refer = sub {
+ # make sure we have a referer header. check_referer doesn't care.
+ return BML::get_client_header('Referer') && $loose_refer->();
+ };
+
+ my $textform = sub {
+ my ( $name, $ml, $optref ) = @_;
+ my %opts = $optref ? %$optref : ();
+ my $text = "\n<label for='$name'>$ML{$ml}</label>";
+ $text .= '<br />' and delete $opts{br} if $opts{br};
+ $text .= LJ::html_text( { name => $name, id => $name, %opts } );
+ return $text;
+ };
+
+ my $checkid = sub {
+ my $id = shift;
+ return $error->( $ML{'error.invalidform'} ) unless $id;
+
+ my $vgift = DW::VirtualGift->new( $id );
+ return $error->( $ML{'.error.badid'} )
+ unless $vgift && $vgift->name;
+
+ return $vgift;
+ };
+
+ my $postform = sub {
+ my ( $mode, $id ) = @_;
+ my $text = "<form action='$action' method='post'>";
+ $text .= LJ::html_hidden( mode => $mode ) if $mode;
+ $text .= LJ::html_hidden( id => $id ) if $id && $id =~ /^\d+$/;
+ return $text;
+ };
+
+ my $endform = sub {
+ my ( $submit ) = @_;
+ my $text = '<p>';
+ $text .= LJ::html_submit( submit => $submit ) ;
+ $text .= '</p></form>';
+ return $text;
+ };
+
+ my $select_tabs = sub {
+ my ( $mode ) = @_;
+ my @modes = ( '', 'tags' ); # ordered
+ my %tabs = (
+ '' => $ML{'.tab.default'},
+ 'tags' => $ML{'.tab.tags'},
+ );
+ return '' unless $tabs{$mode};
+
+ my $text = "<li><b>$ML{'.header.tabs'}</b></li>";
+
+ foreach ( @modes ) {
+ next if $_ eq $mode;
+ my $href = $_ ? "?mode=$_" : $page;
+ $text .= "<li><a href='$href'>$tabs{$_}</a></li>";
+ }
+ return "<ul class='tabs'>$text</ul><div style='clear: both'></div>";
+ };
+
+ my %nonpriv = map { $_ => 1 } DW::VirtualGift->list_nonpriv_tags;
+ my $display_privtags = sub {
+ my $vg = shift;
+ my @print_tags;
+ my $privtext = '';
+ foreach ( sort { $a cmp $b } $vg->tags ) {
+ # linkify
+ my $tagname = LJ::eurl( $_ );
+ my $txt = LJ::ehtml( $_ );
+ $txt = "<a href='tags?mode=view&tag=$tagname'>$txt</a>";
+ unless ( $nonpriv{$_} ) {
+ # asterisk tags with privileges
+ $txt .= ' [*]';
+ $privtext = $ML{'.note.privstar'};
+ }
+ push @print_tags, $txt;
+ }
+ return '' unless @print_tags;
+ return "$ML{'vgift.display.label.tags'} " .
+ join( ', ', @print_tags ) . " $privtext";
+ };
+
+ my $display_gift = sub {
+ my $vg = shift;
+ my $id = $vg->id;
+ my $text = "<div style='margin: 1em 0 2em 0'>";
+ my $check = "${id}_activate";
+ $text .= LJ::html_check( { name => $check, id => $check,
+ value => 1 } );
+ $text .= " <label for='$check'>$ML{'.label.activate'}</label>\n";
+ $text .= LJ::html_hidden( "${id}_chksum" => $vg->checksum );
+ $text .= $vg->display_summary . $display_privtags->( $vg );
+ $text .= "<p><a href='/admin/vgifts/?mode=review&title=inactive&id=$id'>";
+ $text .= BML::ml( '.linktext.edit', { name => $vg->name_ehtml } );
+ $text .= "</a></p></div>";
+ return $text;
+ };
+
+ # end helper routines
+
+ # login check
+ my $remote = LJ::get_remote();
+ return $error->( "<?needlogin?>" ) unless $remote;
+
+ # priv check
+ my @displayprivs = ( "siteadmin:vgifts" );
+ my $siteadmin = $remote->has_priv( 'siteadmin', 'vgifts' ) || $LJ::IS_DEV_SERVER;
+
+ return $error->( BML::ml( "admin.noprivserror",
+ { numprivs => scalar @displayprivs,
+ needprivs => "<b>" . join(", ", @displayprivs) . "</b>"
+ } ) )
+ unless $siteadmin;
+
+ # mode check
+ my $mode = lc( $GET{mode} || $POST{mode} );
+ my $auth = LJ::form_auth();
+ my $linkhome = "<p style='clear: both'><a href='/admin/vgifts/'>"
+ . "$ML{'.linktext.home'}</a></p>\n";
+
+ if ( LJ::did_post() && $mode ) {
+ return $error->( $ML{'error.invalidform'} )
+ unless LJ::check_form_auth();
+ $mode = '' unless $loose_refer->();
+
+ if ( $mode eq 'activate' ) {
+ my @vgs;
+ foreach ( keys %POST ) {
+ my ( $id ) = ( $_ =~ /^(\d+)_activate$/ );
+ next unless $id;
+ my $vg = $checkid->( $id ) or return;
+ next if $vg->is_active; # already active
+
+ return $error->( BML::ml( '.error.notags',
+ { name => $vg->name_ehtml } ) )
+ if $vg->is_untagged;
+ return $error->( BML::ml( '.error.changed',
+ { name => $vg->name_ehtml } ) )
+ if $POST{"${id}_chksum"} ne $vg->checksum;
+
+ push @vgs, $vg;
+ }
+ # now that we're clear of possible errors, do the activation
+ $_->mark_active foreach @vgs;
+ my $ids = join ', ', map { $_->id } @vgs;
+ LJ::statushistory_add( 0, $remote, 'vgifts', "Activated: $ids" )
+ if $ids;
+
+ # go back to where we were
+ $body = BML::redirect( BML::get_client_header('Referer') );
+
+ } else {
+ # if we get here, check_referer failed or something weird happened
+ $body = BML::redirect( $page );
+ }
+
+ return;
+ }
+
+ # non post processing stuff (check for gets)
+
+ # selection tabs go here: List All, Filter By Tag, etc.
+ $body .= $select_tabs->( $mode );
+
+ my @vgs;
+
+ if ( $mode eq 'tags' ) {
+ my $tag = $GET{tag} || '';
+
+ if ( $tag ) {
+ my $id = DW::VirtualGift->get_tagid( $tag )
+ or return $error->( $ML{'.error.badid'} );
+ @vgs = DW::VirtualGift->list_tagged_with( $tag );
+ # list_tagged_with includes active gifts
+ @vgs = grep { $_->is_inactive } @vgs;
+ } else {
+ @vgs = DW::VirtualGift->list_untagged;
+ }
+
+ $body .= "<div class='vgdiv' id='right'>";
+ $body .= "<h2>$ML{'.header.tagfilter'} ";
+ $body .= $tag ? LJ::ehtml( $tag ) : $ML{'.label.untagged'};
+ $body .= "</h2>";
+
+ my $app = DW::VirtualGift->fetch_tagcounts_approved;
+ my $act = DW::VirtualGift->fetch_tagcounts_active;
+ my %approved_inactive;
+ $approved_inactive{$_} = $app->{$_} - $act->{$_}
+ foreach keys %$app;
+
+ if ( %approved_inactive ) {
+ $body .= "<ul style='padding-left: 2em'>";
+ if ( $tag ) {
+ # show entry for untagged filter
+ my $count = scalar grep { $_->is_approved }
+ DW::VirtualGift->list_untagged;
+ if ( $count ) {
+ $body .= "<li><a href='?mode=tags'>";
+ $body .= $ML{'.label.untagged'};
+ $body .= "</a> ($count)</li>";
+ }
+ }
+ foreach ( keys %approved_inactive ) {
+ next if $_ eq $tag;
+ my $count = $approved_inactive{$_} or next;
+ my $tagname = LJ::eurl( $_ );
+ $body .= "<li><a href='?mode=tags&tag=$tagname'>";
+ $body .= LJ::ehtml( $_ );
+ $body .= "</a> ($count)</li>";
+ }
+ $body .= "</ul>";
+ }
+
+ $body .= "</div>";
+
+ } else {
+ # DEFAULT PAGE DISPLAY
+ @vgs = DW::VirtualGift->list_inactive;
+ }
+
+ # don't include queued or rejected gifts
+ @vgs = grep { $_->is_approved } @vgs;
+
+ my @feat = grep { $_->is_featured } @vgs;
+
+ $body .= $postform->( 'activate' ) . $auth;
+
+ $body .= "<h2>$ML{'.header.featured'}</h2>\n";
+ my $text = '';
+ foreach my $vg ( @feat ) {
+ $text .= $display_gift->( $vg );
+ }
+ $body .= $text ? "<ul>$text</ul>" : $ML{'.queue.empty'};
+ $body .= "<div style='clear: left'></div>\n";
+
+ $body .= "<h2>$ML{'.header.nonfeatured'}</h2>\n";
+ $text = '';
+ foreach my $vg ( @vgs ) {
+ next if $vg->is_featured;
+ $text .= $display_gift->( $vg );
+ }
+ $body .= $text ? "<ul>$text</ul>" : $ML{'.queue.empty'};
+ $body .= $endform->( $ML{'.submit.activate'} );
+ $body .= "<div style='margin-bottom: 15px'></div>\n";
+
+ $body .= $linkhome;
+
+ return;
+}
+_code?><?page
+title=><?_code return $title; _code?>
+body=><?_code return $body; _code?>
+head<=
+<style type="text/css">
+label { margin-right: 1em; }
+.vgdiv { margin-bottom: 15px; width: 45%; }
+#content p { margin-top: 1em; }
+#content h2 { margin-top: 1em; }
+#content h3 { margin: 0.5em 0; }
+#content li { margin: 0.25em 0; }
+#content ol { list-style: decimal inside; }
+#left { float: left; }
+#right { float: right; }
+ul.tabs { width: 100%; padding-bottom: 3.5em; }
+ul.tabs li {
+ float: left;
+ display: block;
+ padding-right: 1em;
+ font-size: large;
+}
+ul.tabs li:first-child { padding-right: 0.5em; }
+</style>
+<=head
+page?>
diff -r 4c7678ebddbf -r c4063e1602c6 htdocs/admin/vgifts/inactive.bml.text
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/admin/vgifts/inactive.bml.text Thu Oct 28 12:23:03 2010 +0800
@@ -0,0 +1,37 @@
+.error=Error
+
+.error.badid=The specified tag was not found.
+
+.error.changed=The information for "[[name]]" has changed. Please reload and try again.
+
+.error.notags=The gift "[[name]]" cannot be activated because it has no tags.
+
+.error.upload.noheader=No content-length header: can't upload
+
+.header.featured=Featured Gifts:
+
+.header.nonfeatured=Other Available Gifts:
+
+.header.tabs=Other Views:
+
+.header.tagfilter=Currently viewing:
+
+.label.activate=Activate the following gift:
+
+.label.untagged=Untagged
+
+.linktext.edit=Edit [[name]]'s featured status, cost, or tags.
+
+.linktext.home=Back to Virtual Gifts main page
+
+.note.privstar=(Note: [*] indicates a privileged tag)
+
+.queue.empty=(none available)
+
+.submit.activate=Activate Selected Gifts
+
+.tab.default=List All
+
+.tab.tags=Filter By Tag
+
+.title=Virtual Gifts: Activation Management
diff -r 4c7678ebddbf -r c4063e1602c6 htdocs/admin/vgifts/index.bml
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/admin/vgifts/index.bml Thu Oct 28 12:23:03 2010 +0800
@@ -0,0 +1,673 @@
+<?_code
+{
+ use strict;
+ use vars qw($title $body $page $action %GET %POST);
+
+ use DW::VirtualGift;
+
+ $title = $ML{'.title'};
+ $body = "";
+ $page = "$LJ::SITEROOT/admin/vgifts/";
+ $action = 'index'; # for $postform
+
+ # helper routines
+
+ my $error = sub {
+ $title = $ML{'.error'};
+ $body = join '', @_;
+ return undef;
+ };
+ my $errmsg;
+
+ my $loose_refer = sub {
+ return 1 unless my $refer = BML::get_client_header('Referer');
+ my ( $getargs ) = ( $refer =~ m/\?(.*)$/ );
+ # annoyingly, we get different results for /index vs /
+ return LJ::check_referer( $page ) ||
+ LJ::check_referer( "${page}index" ) unless $getargs;
+
+ return LJ::check_referer( "$page?$getargs" ) ||
+ LJ::check_referer( "${page}index?$getargs" );
+ };
+
+ my $strict_refer = sub {
+ # make sure we have a referer header. check_referer doesn't care.
+ return BML::get_client_header('Referer') && $loose_refer->();
+ };
+
+ my $imgposted = sub {
+ my $id = shift;
+ return length ( $POST{"data_$id"} ) || length ( $POST{"url_$id"} );
+ };
+
+ my $imgform = sub {
+ my $id = shift;
+ my $checkfile = LJ::html_check( { type => 'radio', value => 'file',
+ name => $id, id => "${id}_file",
+ selected => '1',
+ accesskey => $ML{'.label.fromfile.key'}
+ } );
+ my $checkurl = LJ::html_check( { type => 'radio', value => 'url',
+ name => $id, id => "${id}_url",
+ accesskey => $ML{'.label.fromurl.key'}
+ } );
+ my $text = qq:
+ $checkfile
+ <label for='${id}_file'>$ML{'.label.fromfile'}</label>
+ <input type='file' name='data_$id' size='25' /><br />
+ $checkurl
+ <label for='${id}_url'>$ML{'.label.fromurl'}</label>
+ :;
+ $text .= LJ::html_text( { name => "url_$id", size => 25 } ) . "\n";
+ return $text;
+ };
+
+ my $loadpic = sub {
+ my ( $id, $loaderr ) = @_;
+ my $data;
+
+ unless ( LJ::mogclient() ) {
+ $$loaderr = $ML{'.error.upload.nomogilefs'};
+ return undef;
+ }
+
+ if ( $POST{$id} eq 'url' ) {
+ $data = $POST{"url_$id"};
+
+ if ( length( $data ) == 0 ) {
+ $$loaderr = $ML{'.error.upload.nourl'};
+ } elsif ( $data !~ m!^http://! ) {
+ $$loaderr = $ML{'.error.upload.badurl'};
+ } else {
+ my $ua = LJ::get_useragent( role => 'vgift' );
+ my $res = $ua->get( $data );
+ if ( $res && $res->is_success ) {
+ $data = $res->content;
+ } else {
+ $$loaderr = $ML{'.error.upload.urlerror'};
+ }
+ }
+ } elsif ( $POST{$id} eq 'file' ) {
+ $data = $POST{"data_$id"};
+ $$loaderr = $ML{'.error.upload.nofile'} unless length( $data );
+ } else {
+ $$loaderr = $ML{'error.invalidform'};
+ }
+
+ return undef if $$loaderr;
+
+ # further processing
+ my ( $width, $height, $filetype ) = Image::Size::imgsize( \$data );
+ unless ( $width && $height ) {
+ $$loaderr = BML::ml( '.error.upload.badtype',
+ { filetype => $filetype } );
+ } elsif ( ( $width > 100 || $height > 100 ) && $id eq 'img_small' ) {
+ $$loaderr = BML::ml( '.error.upload.dimstoolarge',
+ { imagesize => "${width}x$height",
+ maxsize => "100x100" } );
+ } elsif ( ( $width > 300 || $height > 300 ) && $id eq 'img_large' ) {
+ $$loaderr = BML::ml( '.error.upload.dimstoolarge',
+ { imagesize => "${width}x$height",
+ maxsize => "300x300" } );
+ } elsif ( length( $data ) > 250 * 1024 ) { # 250KB (arbitrary)
+ $$loaderr = BML::ml( '.error.upload.filetoolarge',
+ { maxsize => "250" } );
+ } else {
+ # data should be good, return a reference to it
+ return \$data;
+ }
+ return undef; # check loaderr to see what went wrong
+ };
+
+ my $textform = sub {
+ my ( $name, $ml, $optref ) = @_;
+ my %opts = $optref ? %$optref : ();
+ my $text = "\n<label for='$name'>$ML{$ml}</label>";
+ $text .= '<br />' and delete $opts{br} if $opts{br};
+
+ my $use_area = $opts{textarea} ? 1 : 0;
+ delete $opts{textarea};
+ my $htargs = { name => $name, id => $name, %opts };
+ $text .= $use_area ? LJ::html_textarea( $htargs )
+ : LJ::html_text( $htargs );
+ return $text;
+ };
+
+ my $checkid = sub {
+ my $id = shift;
+ return $error->( $ML{'error.invalidform'} ) unless $id;
+
+ my $vgift = DW::VirtualGift->new( $id );
+ return $error->( $ML{'.error.badid'} )
+ unless $vgift && $vgift->name;
+
+ return $vgift;
+ };
+
+ my $postform = sub {
+ my ( $mode, $id ) = @_;
+ my $text = "<form enctype='multipart/form-data' action='$action' method='post'>";
+ $text .= LJ::html_hidden( mode => $mode ) if $mode;
+ $text .= LJ::html_hidden( id => $id ) if $id && $id =~ /^\d+$/;
+ return $text;
+ };
+
+ my $endform = sub {
+ my ( $submit ) = @_;
+ my $text = '<p>';
+ $text .= LJ::html_submit( submit => $submit ) ;
+ $text .= '</p></form>';
+ return $text;
+ };
+
+ my $shopform = sub {
+ my ( $vg ) = @_;
+ my $id = $vg->id;
+ my $feat = $vg->featured;
+ my $cost = $vg->cost ? $vg->cost : '';
+ my $tags = $vg->display_taglist;
+ my $text = "";
+
+ $text .= "<label for='${id}_featured'>";
+ $text .= $ML{'vgift.display.label.featured'} . '</label>';
+ $text .= LJ::html_select( { name => "${id}_featured",
+ selected => $feat },
+ 'N' => $ML{'.label.review.answer.n'},
+ 'Y' => $ML{'.label.review.answer.y'},
+ );
+ $text .= '<br />';
+ $text .= $textform->( "${id}_cost", 'vgift.display.label.cost',
+ { size => 5, value => $cost } );
+ $text .= BML::ml( 'vgift.display.cost.points',
+ { cost => '' } ) . '<br />';
+ $text .= $textform->( "${id}_tags", 'vgift.display.label.tags',
+ { size => 50, value => $tags } );
+ return $text;
+ };
+
+ my $begin_reviewdiv_left = sub {
+ return "<div style='clear: both'><div class='vgdiv' id='left'>";
+ };
+
+ my $begin_reviewdiv_right = sub {
+ return "</div><div class='vgdiv' id='right'>";
+ };
+
+ my $end_reviewdiv = sub {
+ return "</div></div>\n";
+ };
+
+ my $review_status = sub {
+ my $vg = shift;
+ my $text = "";
+ return $text unless $vg->id && ! $vg->is_queued;
+
+ $text .= "<h3>";
+ $text .= $vg->is_approved
+ ? $ML{'.label.review.approved'}
+ : $ML{'.label.review.rejected'};
+ $text .= $vg->approver->ljuser_display;
+ $text .= "</h3><p><b>$ML{'.label.review.why'}</b> ";
+ $text .= LJ::ehtml( $vg->approved_why );
+ $text .= "</p>\n";
+
+ return $text;
+ };
+
+ my $shop_status = sub {
+ my $vg = shift;
+ my $text = "";
+ return $text unless $vg->id && $vg->is_approved;
+
+ $text .= "<p><b>$ML{'vgift.display.label.featured'}</b> ";
+ $text .= $vg->is_featured
+ ? $ML{'.label.review.answer.y'}
+ : $ML{'.label.review.answer.n'};
+ $text .= "</p><p><b>$ML{'vgift.display.label.cost'}</b> ";
+ $text .= $vg->display_cost . "</p>";
+ # tags are included in display_basic
+
+ return $text;
+ };
+
+ my $review_display = sub {
+ my $vgift = shift;
+ my $status = "";
+ $status .= $review_status->( $vgift );
+ $status .= $shop_status->( $vgift );
+ $status = "<h2>$ML{'.header.review'}</h2>$status" if $status;
+ return $status;
+ };
+
+ my $userview = sub {
+ my ( $u, $review ) = @_;
+ LJ::isu( $u ) or return;
+ $title .= ": " . $u->display_name;
+ my $text = "";
+ foreach my $vg ( DW::VirtualGift->list_created_by( $u ) ) {
+ next unless $vg;
+ my $r = $review && $vg->is_queued;
+ $text .= $begin_reviewdiv_left->();
+ $text .= $vg->display_basic;
+ $text .= "<p>" . $vg->display_vieweditlinks( $r ) . "</p>\n";
+ $text .= $begin_reviewdiv_right->();
+ $text .= $review_display->( $vg );
+ $text .= $end_reviewdiv->();
+ }
+ $body .= $text ? $text : "<p>$ML{'.review.empty'}</p>\n";
+ };
+
+ # end helper routines
+
+ # login check
+ my $remote = LJ::get_remote();
+ return $error->( "<?needlogin?>" ) unless $remote;
+
+ # priv check
+ my @displayprivs = ( "vgifts", "siteadmin:vgifts" );
+ my $siteadmin = $remote->has_priv( 'siteadmin', 'vgifts' ) || $LJ::IS_DEV_SERVER;
+
+ return $error->( BML::ml( "admin.noprivserror",
+ { numprivs => scalar @displayprivs,
+ needprivs => "<b>" . join(", ", @displayprivs) . "</b>"
+ } ) )
+ unless $remote->has_priv( 'vgifts' ) || $siteadmin;
+
+ # process multipart form
+ if ( LJ::did_post() && ! %POST ) {
+ my $r = DW::Request->get;
+ my $size = $r->header_in( "Content-Length" );
+ return $error->( $ML{'.error.upload.noheader'} ) unless $size;
+
+ my $errparse;
+ BML::parse_multipart( \%POST, \$errparse );
+ return $error->( $errparse ) if $errparse;
+
+ # now %POST is set, we can continue
+ }
+
+ # mode check
+ my $mode = lc( $GET{mode} || $POST{mode} );
+ my $linkback = "<p style='clear: both'><a href='$page'>"
+ . "$ML{'.linktext.home'}</a></p>\n";
+
+ if ( LJ::did_post() && $mode ) {
+ return $error->( $ML{'error.invalidform'} )
+ unless LJ::check_form_auth();
+ $mode = '' unless $loose_refer->();
+
+ if ( $mode eq 'create' ) {
+ return $error->( $ML{'.error.create.noname'} )
+ unless $POST{name};
+ return $error->( $ML{'.error.create.nodesc'} )
+ unless $POST{desc};
+
+ if ( $POST{creator} && $siteadmin ) {
+ my $u = LJ::load_user_or_identity( $POST{creator} );
+ return $error->( BML::ml( '.error.create.badusername',
+ { name => $POST{creator} } ) )
+ unless $u && $u->is_individual;
+ $POST{creator} = $u->id;
+ } else {
+ delete $POST{creator}; # siteadmin only
+ }
+
+ return $error->( BML::ml( '.error.denied',
+ { action => $mode } ) )
+ unless $remote->has_priv( 'vgifts' ) || $siteadmin;
+
+ my ( $img_small, $img_large );
+
+ $img_small = $loadpic->( 'img_small', \$errmsg )
+ or return $error->( $errmsg )
+ if $imgposted->( 'img_small' );
+
+ $img_large = $loadpic->( 'img_large', \$errmsg )
+ or return $error->( $errmsg )
+ if $imgposted->( 'img_large' );
+
+ my $vgift = DW::VirtualGift->create( error => \$errmsg,
+ name => $POST{name}, description => $POST{desc},
+ img_small => $img_small, img_large => $img_large,
+ creatorid => $POST{creator} );
+ return $error->( $errmsg ) unless $vgift;
+
+ # hallelujah, the vgift was created.
+ $body = BML::redirect( "$page?mode=view&title=created&id=" . $vgift->id );
+
+ } elsif ( $mode eq 'edit' ) {
+ my $vgift = $checkid->( $POST{id} ) or return;
+
+ return $error->( BML::ml( '.error.denied',
+ { action => $mode } ) )
+ unless $vgift->can_be_edited_by( $remote );
+
+ my ( $img_small, $img_large );
+
+ $img_small = $loadpic->( 'img_small', \$errmsg )
+ or return $error->( $errmsg )
+ if $imgposted->( 'img_small' );
+
+ $img_large = $loadpic->( 'img_large', \$errmsg )
+ or return $error->( $errmsg )
+ if $imgposted->( 'img_large' );
+
+ # Don't honor null attributes.
+ delete $POST{name} unless length $POST{name};
+ delete $POST{desc} unless length $POST{desc};
+
+ # Note: this resets any existing approval status.
+ my $ok = $vgift->edit( error => \$errmsg, approved => '',
+ name => $POST{name}, description => $POST{desc},
+ img_small => $img_small, img_large => $img_large );
+ return $error->( $errmsg ) unless $ok;
+
+ $body = BML::redirect( "$page?mode=view&title=edited&id=" . $vgift->id );
+
+ } elsif ( $mode eq 'approve' ) {
+ my $vgift = $checkid->( $POST{id} ) or return;
+ my $id = $vgift->id;
+
+ return $error->( BML::ml( '.error.denied',
+ { action => $mode } ) )
+ unless $vgift->can_be_approved_by( $remote );
+
+ return $error->( $ML{'.error.yn'} )
+ if exists $POST{"${id}_approve"} && ! $POST{"${id}_approve"};
+
+ return $error->( $ML{'.error.changed'} )
+ if $POST{"${id}_chksum"} ne $vgift->checksum;
+
+ if ( $POST{"${id}_approve"} ) {
+ my $ok = $vgift->edit( error => \$errmsg,
+ approved => $POST{"${id}_approve"},
+ approved_why => $POST{"${id}_comment"},
+ approved_by => $remote->userid );
+ return $error->( $errmsg ) unless $ok;
+
+ $vgift->notify_approved;
+ }
+
+ if ( $POST{"${id}_featured"} || $POST{"${id}_cost"} ) {
+ my %opts;
+ $opts{featured} = $POST{"${id}_featured"} if $POST{"${id}_featured"};
+ $opts{cost} = $POST{"${id}_cost"} if $POST{"${id}_cost"};
+ my $ok = $vgift->edit( error => \$errmsg, %opts );
+ return $error->( $errmsg ) unless $ok;
+ }
+
+ if ( $POST{"${id}_tags"} ) {
+ my $ok = $vgift->tags( $POST{"${id}_tags"},
+ error => \$errmsg, autovivify => $siteadmin );
+ return $error->( $errmsg ) unless $ok;
+ }
+
+ return $body = BML::redirect( "${page}inactive" )
+ if $POST{activation};
+
+ # return to review page for item
+ my $link = "$page?mode=review&id=$id&title=approved";
+ my $days = $POST{days} ? $POST{days} + 0 : 0;
+ $body = $days ? BML::redirect( "$link&days=$days" )
+ : BML::redirect( $link );
+
+ } elsif ( $mode eq 'confirm' ) {
+ my $vgift = $checkid->( $POST{id} ) or return;
+ my $mode = $remote->userid == $vgift->creatorid ? 'view' : 'review';
+
+ my $ok = $vgift->delete( $remote );
+ return $error->( $ML{'.error.delete'} ) unless $ok;
+
+ $body = BML::redirect( "$page?mode=$mode&title=deleted" );
+
+ } else {
+ # if we get here, check_referer failed or something weird happened
+ $body = BML::redirect( $page );
+ }
+
+ return;
+ }
+
+ # non post processing stuff (check for gets)
+ my $auth = LJ::form_auth();
+
+ if ( $mode eq 'view' ) {
+ if ( $GET{id} ) {
+ # view / edit form (priv check)
+ my $vgift = DW::VirtualGift->new( $GET{id} )
+ or return $error->( $ML{'.error.badid'} );
+ my $name = $vgift->name_ehtml
+ or return $error->( $ML{'.error.badid'} );
+ $title = ( $GET{title} && $strict_refer->() )
+ ? $ML{".title.$GET{title}"}
+ : "$title: $name";
+ if ( $vgift->can_be_edited_by( $remote ) ) {
+ # form for editing
+ $body .= "<h2>$name (#" . $vgift->id . ")</h2>\n<h3>";
+ $body .= BML::ml( 'vgift.display.createdby',
+ { user => $vgift->creator->ljuser_display,
+ ago => $vgift->created_ago_text } ) . "</h3>\n";
+ $body .= $postform->( 'edit', $vgift->id ) . $auth;
+ $body .= $textform->( 'name', '.label.edit.name',
+ { size => 40, maxlength => 80 } );
+ $body .= "<p><b>$ML{'.label.create.desc'}</b> ";
+ $body .= $vgift->description_ehtml . "</p>";
+ $body .= $textform->( 'desc', '.label.edit.desc',
+ { size => 40, maxlength => 255 } );
+ $body .= "<h3>$ML{'.header.imgsmall'}</h3>\n";
+ $body .= $vgift->img_small_html;
+ $body .= "<br /><label for='img_small'>$ML{'.label.edit.imgsmall'}</label>";
+ $body .= "<br />" . $imgform->( 'img_small' );
+ $body .= "<h3>$ML{'.header.imglarge'}</h3>\n";
+ $body .= $vgift->img_large_html;
+ $body .= "<br /><label for='img_large'>$ML{'.label.edit.imglarge'}</label>";
+ $body .= "<br />" . $imgform->( 'img_large' );
+ $body .= $endform->( $ML{'.submit.edit'} );
+ } else {
+ # view only
+ $body .= $begin_reviewdiv_left->();
+ $body .= $vgift->display_basic;
+ $body .= $vgift->img_large_html;
+ $body .= $begin_reviewdiv_right->();
+ $body .= $review_display->( $vgift );
+ $body .= $end_reviewdiv->();
+ }
+ } elsif ( $GET{user} ) {
+ $userview->( LJ::load_user( $GET{user} ), $siteadmin );
+ $body = BML::redirect( $page ) and return unless $body;
+ } else { # view summary for logged in user
+ if ( $GET{title} eq 'deleted' && $strict_refer->() ) {
+ $body .= "<?warningbar " . $ML{'.review.deleted'};
+ $body .= " warningbar?>\n";
+ }
+ $userview->( $remote );
+ }
+ $body .= $linkback;
+
+ } elsif ( $mode eq 'review' ) {
+ # review queue for siteadmins
+ $title .= ": " . $ML{'.title.review'};
+ $body = BML::redirect( $page ) and return unless $siteadmin;
+
+ my $feedback = "";
+ if ( $GET{title} eq 'deleted' && $strict_refer->() ) {
+ $feedback .= "<?warningbar " . $ML{'.review.deleted'};
+ $feedback .= " warningbar?>\n";
+ }
+ if ( $GET{id} && $GET{title} eq 'approved' && $strict_refer->() ) {
+ my $vg = DW::VirtualGift->new( $GET{id} )
+ or return $error->( $ML{'.error.badid'} );
+ my $name = $vg->name_ehtml
+ or return $error->( $ML{'.error.badid'} );
+ $feedback .= "<?warningbar " . $ML{'.review.approved'};
+ $feedback .= " $name (#$GET{id}) warningbar?>\n";
+ }
+
+ my $inactive = $GET{title} && $GET{title} eq 'inactive' ? 1 : 0;
+ my $days = $GET{days} ? $GET{days} + 0 : 0;
+ my @vgifts;
+
+ if ( $GET{id} ) {
+ @vgifts = ( DW::VirtualGift->new( $GET{id} ) );
+
+ if ( $inactive ) {
+ $linkback = "<p style='clear: both'>"
+ . "<a href='inactive'>"
+ . "$ML{'.linktext.inactive'}</a></p>\n";
+ } else {
+ # link back to queue instead of main page
+ $linkback = $days
+ ? "<p style='clear: both'>"
+ . "<a href='?mode=review&days=$days'>"
+ . "$ML{'.linktext.review.recent'}</a></p>\n"
+ : "<p style='clear: both'>"
+ . "<a href='?mode=review'>"
+ . "$ML{'.linktext.review.all'}</a></p>\n";
+ }
+ } else {
+ @vgifts = $days
+ ? DW::VirtualGift->list_recent( $days )
+ : DW::VirtualGift->list_queued();
+ }
+ foreach my $vg ( @vgifts ) {
+ next unless $vg && $vg->can_be_approved_by( $remote );
+ $body .= $begin_reviewdiv_left->();
+ $body .= $vg->display_basic;
+ $body .= "<p>" . $vg->display_vieweditlinks . "</p>\n";
+ $body .= $begin_reviewdiv_right->();
+ $body .= "<h2>$ML{'.header.review'}</h2>";
+ # status display for approved and rejected gifts
+ $body .= $review_status->( $vg ) unless $vg->is_queued;
+
+ # display approval form
+ unless ( $vg->is_rejected ) {
+ my $id = $vg->id;
+ $body .= $postform->( 'approve', $id ) . $auth;
+ $body .= LJ::html_hidden( days => $days ) if $days;
+ $body .= LJ::html_hidden( "${id}_chksum" => $vg->checksum );
+
+ if ( $vg->is_queued ) {
+ $body .= "<label for='${id}_approve'>";
+ $body .= $ML{'.label.review.approval'} . '</label>';
+ $body .= LJ::html_select( { name => "${id}_approve", selected => '' },
+ '' => '',
+ 'Y' => $ML{'.label.review.answer.y'},
+ 'N' => $ML{'.label.review.answer.n'},
+ );
+ $body .= '<br />';
+ $body .= $textform->( "${id}_comment", '.label.review.comment',
+ { textarea => 1, cols => 40, rows => 10, br => 1 } );
+ } elsif ( $vg->is_approved ) {
+ # prompt for suggestions
+ $body .= "<h3>" . $ML{'.label.review.optional'} . "</h3>";
+ $body .= LJ::html_hidden( activation => $inactive );
+ $body .= $shopform->( $vg );
+ }
+ $body .= $endform->( $ML{'.submit.review'} );
+ }
+ $body .= $end_reviewdiv->();
+ }
+ $body ||= $ML{'.review.empty'};
+ $body = $feedback . $body . $linkback;
+
+ } elsif ( $mode eq 'delete' ) {
+ $title = $ML{'.title.delete'};
+ $body = BML::redirect( $page ) and return unless $GET{id};
+ my $vgift = $checkid->( $GET{id} ) or return;
+ if ( $vgift->can_be_deleted_by( $remote ) ) {
+ $body .= '<h3>' . $ML{'.header.delete'} . '</h3>';
+ $body .= $vgift->display_basic;
+ $body .= $postform->( 'confirm', $vgift->id ) . $auth;
+ $body .= $endform->( $ML{'.submit.delete'} );
+ } else { # can't be deleted
+ $body .= "<p>$ML{'.error.delete'}</p>";
+ }
+ $body .= $linkback;
+
+ } elsif ( $mode eq 'artists' ) {
+ $title .= ": $ML{'.title.artists'}";
+ $body .= '<ol>';
+ $body .= DW::VirtualGift->display_creatorlist;
+ $body .= "</ol>$linkback";
+
+ } else {
+ # DEFAULT PAGE DISPLAY
+ $body .= "<div class='vgdiv' id='right' style='text-align: center'>";
+ $body .= "<h2>$ML{'.header.userqueue'}</h2>";
+ my @vgifts = DW::VirtualGift->list_created_by( $remote );
+ my $queue = "";
+ foreach my $vg ( @vgifts ) {
+ next unless $vg && $vg->can_be_edited_by( $remote );
+ $queue .= '<li>"' . $vg->name_ehtml . '"';
+ $queue .= ' (' . $vg->created_ago_text . ') ';
+ $queue .= $vg->display_vieweditlinks . "</li>\n";
+ }
+ $queue .= "<li style='padding-top: 0.5em'><a href='?mode=view'>$ML{'.linktext.viewall'}</a></li>\n"
+ if $queue;
+
+ $body .= $queue ? "\n<ul>$queue</ul>" : "<i>$ML{'.queue.empty'}</i>";
+
+ $body .= "<h2>$ML{'.header.artists'}</h2>";
+ # reuse $queue for building string
+ $queue = DW::VirtualGift->display_creatorlist( 5 );
+ $queue .= "<li style='padding-top: 0.5em'><a href='?mode=artists'>$ML{'.linktext.viewall'}</a></li>\n"
+ if $queue;
+
+ $body .= $queue ? "\n<ul>$queue</ul>" : "<i>$ML{'.queue.empty'}</i>";
+
+ if ( $siteadmin ) {
+ $body .= qq|
+ <h2>$ML{'.header.siteadmin'}</h2>
+ <ul>
+ <li><a href='?mode=review&days=14'>
+ $ML{'.linktext.review.recent'}</a></li>
+ <li><a href='?mode=review'>
+ $ML{'.linktext.review.all'}</a></li>
+ <li><a href='tags'>$ML{'.linktext.tags'}</a></li>
+ <li><a href='inactive'>$ML{'.linktext.inactive'}</a></li>
+ </ul>
+ |;
+ }
+
+ $body .= "</div><div class='vgdiv' id='left'>";
+ $body .= "<h2>$ML{'.header.create'}</h2>";
+ $body .= $postform->( 'create' ) . $auth;
+ $body .= $textform->( 'name', '.label.create.name',
+ { size => 40, maxlength => 80, br => 1 } );
+ $body .= '<br />';
+ $body .= $textform->( 'desc', '.label.create.desc',
+ { size => 40, maxlength => 255, br => 1 } );
+ if ( $siteadmin ) {
+ $body .= '<br />';
+ $body .= $textform->( 'creator', '.label.create.creator',
+ { size => 40, maxlength => 80, br => 1 } );
+ }
+
+ $body .= "<h3>$ML{'.header.imgsmall'}</h3>\n";
+ $body .= $imgform->( 'img_small' );
+ $body .= "<h3>$ML{'.header.imglarge'}</h3>\n";
+ $body .= $imgform->( 'img_large' );
+ $body .= "<p>$ML{'.note.svg'}</p>";
+ $body .= $endform->( $ML{'.submit.create'} );
+ $body .= "</div>\n";
+ }
+
+ return;
+}
+_code?><?page
+title=><?_code return $title; _code?>
+body=><?_code return $body; _code?>
+head<=
+<style type="text/css">
+label { margin-right: 1em; }
+.vgdiv { margin-bottom: 15px; width: 45%; }
+#content p { margin-top: 1em; }
+#content h2 { margin-top: 1em; }
+#content h3 { margin: 0.5em 0; }
+#content li { margin: 0.25em 0; }
+#content ol { list-style: decimal inside; }
+#left { float: left; }
+#right { float: right; }
+</style>
+<=head
+page?>
diff -r 4c7678ebddbf -r c4063e1602c6 htdocs/admin/vgifts/index.bml.text
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/admin/vgifts/index.bml.text Thu Oct 28 12:23:03 2010 +0800
@@ -0,0 +1,133 @@
+.error=Error
+
+.error.badid=An error occurred while trying to load this vgift.
+
+.error.changed=The information for this virtual gift has changed. Please reload and try again.
+
+.error.create.badusername=The username you specified ([[name]]) does not appear to be a valid personal or identity account.
+
+.error.create.nodesc=Please give your new gift a description.
+
+.error.create.noname=Please give your new gift a name.
+
+.error.delete=This gift cannot be deleted, either because you don't have the required permissions or because it has already been made available for sale.
+
+.error.denied=You do not have permission to [[action]] this gift.
+
+.error.upload.badtype=Files of type [[filetype]] are not supported. You can only upload GIF, PNG or JPG files.
+
+.error.upload.badurl=The address for the picture to be uploaded does not look correct. It should start with <b>http://</b>
+
+.error.upload.dimstoolarge=The dimensions of your image ([[imagesize]]) exceed maximum size. Your picture can't be larger than [[maxsize]] pixels.
+
+.error.upload.filetoolarge=Image uploaded is too large. File size cannot exceed [[maxsize]] KB.
+
+.error.upload.nofile=You must choose a file to upload
+
+.error.upload.noheader=No content-length header: can't upload
+
+.error.upload.nomogilefs=Couldn't load MogileFS: can't upload
+
+.error.upload.nourl=You must enter the URL of an image
+
+.error.upload.urlerror=An error ocurred while trying to fetch your image.
+
+.error.yn=Please select Yes or No.
+
+.header.artists=Leaderboard
+
+.header.create=Create a new virtual gift
+
+.header.delete=Are you sure?
+
+.header.imglarge=Large image
+
+.header.imgsmall=Small (main) image
+
+.header.review=Review:
+
+.header.siteadmin=Site Admins:
+
+.header.userqueue=Your other pending submissions:
+
+.label.create.creator=Username to credit as creator <u>(admin only)</u>:
+
+.label.create.desc=Description (for image alt text):
+
+.label.create.name=Name (must be unique):
+
+.label.edit.desc=New description?
+
+.label.edit.imglarge=Choose different large image:
+
+.label.edit.imgsmall=Choose different small image:
+
+.label.edit.name=New name?
+
+.label.fromfile=From <u>F</u>ile:
+
+.label.fromfile.key|notes=Enter here a <b>lower-case</b> version of the letter you underlined in ".fromfile". This is the shortcut key for the fromfile option.
+.label.fromfile.key=f
+
+.label.fromurl=From U<u>R</u>L:
+
+.label.fromurl.key|notes=Enter here a <b>lower-case</b> version of the letter you underlined in ".fromurl". This is the shortcut key for the fromurl option.
+.label.fromurl.key=r
+
+.label.review.answer.n=No
+
+.label.review.answer.y=Yes
+
+.label.review.approval=Approve?
+
+.label.review.approved=<span style="color: green">Approved by:</span>
+
+.label.review.comment=Comment:
+
+.label.review.optional=Optional: suggestions for shop qualifiers
+
+.label.review.rejected=<span style="color: red">Rejected by:</span>
+
+.label.review.why=<b>Comment: </b>
+
+.linktext.home=Back to Virtual Gifts main page
+
+.linktext.inactive=Manage Inactive Gifts
+
+.linktext.review.all=View All Pending Submissions
+
+.linktext.review.recent=View Recent Submissions
+
+.linktext.tags=Manage Tags
+
+.linktext.viewall=View All
+
+.note.svg=<i><b>Note:</b> If you have a scalable vector graphic, you will need to submit it via another method.</i>
+
+.queue.empty=(none available)
+
+.review.approved=Approval changes submitted:
+
+.review.deleted=The gift was deleted.
+
+.review.empty=There are no relevant gifts available to display.
+
+.submit.create=Create
+
+.submit.edit=Submit Changes
+
+.submit.delete=Yes, I want to delete this gift.
+
+.submit.review=Submit Review
+
+.title=Virtual Gifts
+
+.title.artists=Artists
+
+.title.created=Creation Successful!
+
+.title.delete=Delete A Gift
+
+.title.edited=Edit Successful!
+
+.title.review=Review Queue
diff -r 4c7678ebddbf -r c4063e1602c6 htdocs/admin/vgifts/tags.bml
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/admin/vgifts/tags.bml Thu Oct 28 12:23:03 2010 +0800
@@ -0,0 +1,308 @@
+<?_code
+{
+ use strict;
+ use vars qw($title $body $page $action %GET %POST);
+
+ use DW::VirtualGift;
+
+ $title = $ML{'.title'};
+ $body = "";
+ $page = "$LJ::SITEROOT/admin/vgifts/tags";
+ $action = 'tags'; # for $postform
+
+ # helper routines
+
+ my $error = sub {
+ $title = $ML{'.error'};
+ $body = join '', @_;
+ return undef;
+ };
+ my $errmsg;
+
+ my $loose_refer = sub {
+ return 1 unless my $refer = BML::get_client_header('Referer');
+ my ( $getargs ) = ( $refer =~ m/\?(.*)$/ );
+ return LJ::check_referer( $page ) unless $getargs;
+ return LJ::check_referer( "$page?$getargs" );
+ };
+
+ my $strict_refer = sub {
+ # make sure we have a referer header. check_referer doesn't care.
+ return BML::get_client_header('Referer') && $loose_refer->();
+ };
+
+ my $textform = sub {
+ my ( $name, $ml, $optref ) = @_;
+ my %opts = $optref ? %$optref : ();
+ my $text = "\n<label for='$name'>$ML{$ml}</label>";
+ $text .= '<br />' and delete $opts{br} if $opts{br};
+ $text .= LJ::html_text( { name => $name, id => $name, %opts } );
+ return $text;
+ };
+
+ my $checkid = sub {
+ my $id = shift;
+ return $error->( $ML{'error.invalidform'} ) unless $id;
+
+ my $vgift = DW::VirtualGift->new( $id );
+ return $error->( $ML{'.error.badid'} )
+ unless $vgift && $vgift->name;
+
+ return $vgift;
+ };
+
+ my $postform = sub {
+ my ( $mode, $id ) = @_;
+ my $text = "<form action='$action' method='post'>";
+ $text .= LJ::html_hidden( mode => $mode ) if $mode;
+ $text .= LJ::html_hidden( id => $id ) if $id && $id =~ /^\d+$/;
+ return $text;
+ };
+
+ my $endform = sub {
+ my ( $submit ) = @_;
+ my $text = '<p>';
+ $text .= LJ::html_submit( submit => $submit ) ;
+ $text .= '</p></form>';
+ return $text;
+ };
+
+ # end helper routines
+
+ # login check
+ my $remote = LJ::get_remote();
+ return $error->( "<?needlogin?>" ) unless $remote;
+
+ # priv check
+ my @displayprivs = ( "siteadmin:vgifts" );
+ my $siteadmin = $remote->has_priv( 'siteadmin', 'vgifts' ) || $LJ::IS_DEV_SERVER;
+
+ return $error->( BML::ml( "admin.noprivserror",
+ { numprivs => scalar @displayprivs,
+ needprivs => "<b>" . join(", ", @displayprivs) . "</b>"
+ } ) )
+ unless $siteadmin;
+
+ # mode check
+ my $mode = lc( $GET{mode} || $POST{mode} );
+ my $linkback = "<p style='clear: both'><a href='$page'>"
+ . BML::ml( '.linktext.back', { title => $ML{'.title'} } )
+ . "</a></p>\n";
+ my $linkhome = "<p style='clear: both'><a href='/admin/vgifts/'>"
+ . "$ML{'.linktext.home'}</a></p>\n";
+
+ if ( LJ::did_post() && $mode ) {
+ return $error->( $ML{'error.invalidform'} )
+ unless LJ::check_form_auth();
+ $mode = '' unless $loose_refer->();
+
+ if ( $mode eq 'edit' ) {
+ my $tagid = $POST{id}
+ or return $error->( $ML{'.error.badid'} );
+ my $tag = DW::VirtualGift->get_tagname( $tagid )
+ or return $error->( $ML{'.error.badid'} );
+
+ return $error->( $ML{'.error.needpriv'} )
+ if $POST{"${tagid}_privarg"} && ! $POST{"${tagid}_addpriv"};
+
+ # make sure the new priv is valid
+ my $priv = $POST{"${tagid}_addpriv"};
+ my $addpriv_id;
+ if ( $priv ) {
+ $addpriv_id = DW::VirtualGift->validate_priv( $priv );
+ return $error->( LJ::Lang::ml( '.error.badpriv',
+ { priv => LJ::ehtml( $priv ) } ) )
+ unless $addpriv_id;
+ }
+
+ # also validate arg if given
+ my $arg = $POST{"${tagid}_privarg"};
+ if ( $arg && $arg ne '*' ) {
+ my $valid_args = LJ::list_valid_args( $priv );
+ return $error->( LJ::Lang::ml( '.error.badarg',
+ { priv => LJ::ehtml( $priv ),
+ arg => LJ::ehtml( $arg ) } ) )
+ unless $valid_args && $valid_args->{$arg};
+ }
+
+ # process rename first
+ if ( my $newtag = $POST{"${tagid}_rename"} ) {
+ DW::VirtualGift->alter_tag( $tag, $newtag )
+ or return $error->( LJ::Lang::ml( '.error.badtagname',
+ { tag => LJ::ehtml( $newtag ) } ) );
+ $tag = $newtag; # subsequent changes target $newtag
+ }
+
+ # process new privilege
+ if ( $addpriv_id ) {
+ DW::VirtualGift->add_priv_to_tag( $tag, $priv, $arg )
+ or return $error->( LJ::Lang::ml( '.error.privarg',
+ { privarg => LJ::ehtml( "$priv:$arg" ) } ) );
+ }
+
+ # process old privileges
+ if ( $POST{"${tagid}_maxprivnum"} ) {
+ # only remove existing privs if not renamed (can resubmit)
+ unless ( $POST{"${tagid}_rename"} ) {
+ DW::VirtualGift->remove_priv_from_tag( $tag, $_->[0], $_->[1] )
+ foreach DW::VirtualGift->list_tagprivs( $tag );
+ }
+ # add back selected privs
+ foreach my $i ( 0..$POST{"${tagid}_maxprivnum"} ) {
+ if ( $POST{"${tagid}_priv$i"} ) {
+ my ( $priv, $arg ) =
+ ( $POST{"${tagid}_priv$i"} =~ /^([^:]+):(.*)$/ );
+ next unless $priv;
+ DW::VirtualGift->add_priv_to_tag( $tag, $priv, $arg )
+ or return $error->( BML::ml( '.error.privarg',
+ { privarg => LJ::ehtml( "$priv:$arg" ) } ) );
+ }
+ }
+ }
+
+ $tagid = DW::VirtualGift->get_tagid( $tag )
+ if $POST{"${tagid}_rename"};
+ $body = BML::redirect( "$page?mode=view&title=edited&id=$tagid" );
+
+ } elsif ( $mode eq 'confirm' ) {
+ my $tag = DW::VirtualGift->get_tagname( $POST{id} )
+ or return $error->( $ML{'.error.badid'} );
+ DW::VirtualGift->alter_tag( $tag );
+ $body = BML::redirect( $page );
+
+ } else {
+ # if we get here, check_referer failed or something weird happened
+ $body = BML::redirect( $page );
+ }
+
+ return;
+ }
+
+ # non post processing stuff (check for gets)
+ my $auth = LJ::form_auth();
+ my $tag = LJ::durl( $GET{tag} || '' );
+ my $id;
+
+ if ( $mode ) {
+ $body = BML::redirect( $page ) and return unless $tag;
+ $id = DW::VirtualGift->get_tagid( $tag )
+ or return $error->( $ML{'.error.badid'} );
+ }
+
+ if ( $mode eq 'view' ) {
+ my $etag = LJ::ehtml( $tag );
+ my $urltag = LJ::eurl( $tag );
+ $title = ( $GET{title} && $strict_refer->() )
+ ? $ML{".title.$GET{title}"}
+ : "$title: $etag";
+ $body .= "<h2>$ML{'.label.edit.tagname'}: <b>$etag</b> ";
+ $body .= "<a href='?mode=delete&tag=$urltag'>";
+ $body .= "$ML{'.linktext.deletetag'}</a></h2>\n";
+ $body .= $postform->( 'edit', $id ) . $auth;
+ $body .= '<p>';
+ $body .= $textform->( "${id}_rename", '.label.edit.name',
+ { size => 25, maxlength => 40 } );
+ $body .= "</p><p><i>$ML{'.note.tagmerge'}</i></p><p>";
+ $body .= $textform->( "${id}_addpriv", '.label.edit.priv',
+ { size => 15, maxlength => 25 } );
+ $body .= ' : ';
+ $body .= LJ::html_text( { name => "${id}_privarg",
+ id => "${id}_privarg",
+ size => 15, maxlength => 25 } );
+ $body .= '</p>';
+
+ $body .= "<h3>$ML{'.header.privlist'}</h3>\n";
+ my @privarg = DW::VirtualGift->list_tagprivs( $tag );
+ my $list = "";
+ foreach my $i ( 0..$#privarg ) {
+ my ( $priv, $arg ) = @{ $privarg[$i] };
+ $list .= '<li>';
+ $list .= LJ::html_check( { value => "$priv:$arg",
+ selected => 1,
+ name => "${id}_priv$i",
+ id => "${id}_priv$i" } );
+ $list .= $arg? " $priv:$arg" : " $priv";
+ $list .= '</li>';
+ }
+ if ( $list ) {
+ $body .= "<ul>$list</ul><i>$ML{'.note.removeprivs'}</i>";
+ $body .= LJ::html_hidden( "${id}_maxprivnum" => $#privarg );
+ } else {
+ $body .= $ML{'.queue.empty'};
+ }
+ $body .= $endform->( $ML{'.submit.edit'} );
+
+ $body .= "<h3>$ML{'.header.giftlist'}</h3>\n";
+ my @vgifts = DW::VirtualGift->list_tagged_with( $tag );
+ $list = "";
+ foreach my $vg ( @vgifts ) {
+ $list .= '<li>' . $vg->name_ehtml . ' (#' . $vg->id . ') ';
+ $list .= $vg->is_approved ? ''
+ : "<b>$ML{'.note.notapproved'}</b> ";
+ $list .= $vg->is_inactive ? ''
+ : "<b>$ML{'.note.active'}</b> ";
+ $list .= $vg->display_vieweditlinks( $vg->is_queued );
+ $list .= " | <a href='?mode=remove&tag=$urltag&vg=";
+ $list .= $vg->id . "'>$ML{'.linktext.removetag'}</a></li>";
+ }
+ $body .= $list ? "<ul>$list</ul>" : $ML{'.review.empty'};
+ $body .= $linkback;
+
+ } elsif ( $mode eq 'delete' ) {
+ $title .= ": $ML{'.title.delete'}";
+ my $etag = LJ::ehtml( $tag );
+ $body .= '<h2>' . BML::ml( '.header.delete', { tag => $etag } ) . '</h2>';
+ $body .= $postform->( 'confirm', $id ) . $auth;
+ $body .= $endform->( $ML{'.submit.delete'} );
+ $body .= $linkback;
+
+ } elsif ( $mode eq 'remove' ) {
+ my $vg = $checkid->( $GET{vg} ) or return;
+ # quickly process and return
+ $vg->remove_tag_by_id( $id );
+ $body = BML::redirect( "$page?mode=view&tag=" . LJ::eurl( $tag ) );
+ } else {
+ # DEFAULT PAGE DISPLAY
+ my $counts = DW::VirtualGift->fetch_tagcounts_approved;
+ my %nonpriv = map { $_ => $counts->{$_} }
+ DW::VirtualGift->list_nonpriv_tags;
+ delete @$counts{ keys %nonpriv }; # leaving only privileged data
+
+ my $printtags = sub {
+ my $hr = shift;
+ my $queue = "";
+ foreach my $tag ( sort keys %$hr ) {
+ my $urltag = LJ::eurl( $tag );
+ my $etag = LJ::ehtml( $tag );
+ $queue .= "<h3>$etag (" . $hr->{$tag} . ") ";
+ $queue .= "<a href='?mode=view&tag=$urltag'>";
+ $queue .= "$ML{'vgift.display.linktext.viewedit'}</a> | ";
+ $queue .= "<a href='?mode=delete&tag=$urltag'>";
+ $queue .= "$ML{'.linktext.deletetag'}</a></h3>\n";
+ }
+ return $queue ? $queue : "<h3>$ML{'.queue.empty'}</h3>";
+ };
+
+ $body .= "<h2>$ML{'.header.priv'}</h2>\n";
+ $body .= $printtags->( $counts );
+ $body .= "<h2>$ML{'.header.nonpriv'}</h2>\n";
+ $body .= $printtags->( \%nonpriv );
+ $body .= $linkhome;
+ }
+
+ return;
+}
+_code?><?page
+title=><?_code return $title; _code?>
+body=><?_code return $body; _code?>
+head<=
+<style type="text/css">
+label { margin-right: 1em; }
+#content p { margin-top: 1em; }
+#content h2 { margin-top: 1em; }
+#content h3 { margin: 0.5em 0; }
+#content li { margin: 0.25em 0; }
+#content ol { list-style: decimal inside; }
+</style>
+<=head
+page?>
diff -r 4c7678ebddbf -r c4063e1602c6 htdocs/admin/vgifts/tags.bml.text
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/admin/vgifts/tags.bml.text Thu Oct 28 12:23:03 2010 +0800
@@ -0,0 +1,61 @@
+.error=Error
+
+.error.badarg=The argument [[arg]] is not valid for the privilege [[priv]].
+
+.error.badid=The specified tag was not found.
+
+.error.badpriv=There is no defined privilege named [[priv]].
+
+.error.badtagname=Could not rename to [[tag]].
+
+.error.needpriv=You specified an arg but failed to specify an associated privilege.
+
+.error.privarg=There was a problem adding [[privarg]] permissions to the specified tag.
+
+.error.upload.noheader=No content-length header: can't upload
+
+.header.delete=Are you sure you want to delete the tag '[[tag]]' from all gifts?
+
+.header.giftlist=Current gifts with this tag:
+
+.header.nonpriv=Nonprivileged (Public) Tags
+
+.header.priv=Privileged (Restricted) Tags
+
+.header.privlist=Current restrictions:
+
+.label.edit.name=New name:
+
+.label.edit.priv=Restrict to users with the following privilege:
+
+.label.edit.tagname=Tag Name
+
+.linktext.back=Back to [[title]]
+
+.linktext.deletetag=[Delete This Tag]
+
+.linktext.home=Back to Virtual Gifts main page
+
+.linktext.removetag=[Remove This Tag]
+
+.note.active=(Active)
+
+.note.notapproved=(Not Approved)
+
+.note.removeprivs=Uncheck boxes next to any restrictions you want removed.
+
+.note.tagmerge=Existing tag names are allowed; privileges will be merged.
+
+.queue.empty=(none available)
+
+.review.empty=There are no relevant gifts available to display.
+
+.submit.edit=Submit Changes
+
+.submit.delete=Yes, I want to delete this tag.
+
+.title=Virtual Gifts: Tag Management
+
+.title.delete=Delete A Tag
+
+.title.edited=Edit Successful!
diff -r 4c7678ebddbf -r c4063e1602c6 views/admin/index.tt.text
--- a/views/admin/index.tt.text Thu Oct 28 11:15:07 2010 +0800
+++ b/views/admin/index.tt.text Thu Oct 28 12:23:03 2010 +0800
@@ -83,6 +83,9 @@
.admin.userlog.link=Userlog Viewer
.admin.userlog.text=Shows you a user's logged actions.
+.admin.vgifts.link=Virtual Gifts
+.admin.vgifts.text=Maintain the virtual gift shop and submit new gift ideas.
+
.anysupportpriv=any support priv
.devserver=dev server
--------------------------------------------------------------------------------

no subject
no subject
no subject
web.ads.advertisement=<a [[aopts]]>Advertisement</a>
web.ads.advertisement_nolink=Advertisement
need to cleanup
no subject
no subject
no subject