1264 lines
38 KiB
Perl
1264 lines
38 KiB
Perl
|
|
#!/usr/bin/perl
|
||
|
|
#
|
||
|
|
# DW::VirtualGift - Provide virtual gifts for users
|
||
|
|
#
|
||
|
|
# Authors:
|
||
|
|
# Jen Griffin <kareila@livejournal.com>
|
||
|
|
#
|
||
|
|
# Copyright (c) 2010-2013 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 DW::BlobStore;
|
||
|
|
|
||
|
|
use LJ::Global::Constants;
|
||
|
|
|
||
|
|
# Because events use this module, Perl warns about redefined subroutines.
|
||
|
|
{
|
||
|
|
no warnings 'redefine';
|
||
|
|
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)
|
||
|
|
|
||
|
|
# Transaction methods moved to DW::VirtualGiftTransaction
|
||
|
|
|
||
|
|
# 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 media storage (blobstore).
|
||
|
|
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;
|
||
|
|
|
||
|
|
# initialize this gift in the vgift_counts table
|
||
|
|
$dbh->do( "INSERT INTO vgift_counts (vgiftid,count) VALUES (?,0)", undef, $self->id );
|
||
|
|
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 DW::BlobStore->store( vgifts => $key, $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 media storage (blobstore).
|
||
|
|
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 mark_sold {
|
||
|
|
my ($self) = @_;
|
||
|
|
return undef unless $self->id;
|
||
|
|
|
||
|
|
my $dbh = LJ::get_db_writer();
|
||
|
|
$dbh->do( "UPDATE vgift_counts SET count=count+1 WHERE vgiftid=?", undef, $self->id );
|
||
|
|
die $dbh->errstr if $dbh->err;
|
||
|
|
|
||
|
|
LJ::MemCache::delete( $self->num_sold_memkey );
|
||
|
|
return $self;
|
||
|
|
}
|
||
|
|
|
||
|
|
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 ? () : [];
|
||
|
|
}
|
||
|
|
|
||
|
|
# make sure the tags we've specified are valid
|
||
|
|
my @valid_tags;
|
||
|
|
foreach my $tag (@newtags) {
|
||
|
|
my ( $bytes, $chars ) = LJ::text_length($tag);
|
||
|
|
next if $bytes > LJ::BMAX_SITEKEYWORD;
|
||
|
|
next if $chars > LJ::CMAX_SITEKEYWORD;
|
||
|
|
next if $tag =~ /[\<\>]/;
|
||
|
|
push @valid_tags, $tag;
|
||
|
|
}
|
||
|
|
|
||
|
|
my %invalid_tags = map { $_ => 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, allowmixedcase => 1 );
|
||
|
|
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, allowmixedcase => 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 storage
|
||
|
|
DW::BlobStore->delete( vgifts => $self->img_mogkey('large') );
|
||
|
|
DW::BlobStore->delete( vgifts => $self->img_mogkey('small') );
|
||
|
|
|
||
|
|
# wipe the relevant rows from the database
|
||
|
|
$self->_tagwipe;
|
||
|
|
my $dbh = LJ::get_db_writer();
|
||
|
|
$dbh->do("DELETE FROM vgift_ids WHERE vgiftid=$id");
|
||
|
|
die $dbh->errstr if $dbh->err;
|
||
|
|
$dbh->do("DELETE FROM vgift_counts WHERE vgiftid=$id");
|
||
|
|
die $dbh->errstr if $dbh->err;
|
||
|
|
|
||
|
|
# wipe the relevant keys from memcache
|
||
|
|
LJ::MemCache::delete( $self->num_sold_memkey );
|
||
|
|
$self->_expire_relevant_keys;
|
||
|
|
$self->_remove_from_memcache; # LJ::MemCacheable
|
||
|
|
|
||
|
|
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, allowmixedcase => 1 ) }
|
||
|
|
|
||
|
|
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;
|
||
|
|
|
||
|
|
# if the vgift has been purchased, don't allow
|
||
|
|
return 0 if $self->num_sold;
|
||
|
|
|
||
|
|
# 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';
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach my $size (qw( large small )) {
|
||
|
|
my $data = DW::BlobStore->retrieve( vgifts => $self->img_mogkey($size) );
|
||
|
|
push @attrvals, ref $data eq 'SCALAR' ? $$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
|
||
|
|
}
|
||
|
|
|
||
|
|
sub num_sold {
|
||
|
|
my ($self) = @_;
|
||
|
|
my $id = $self->id or return undef;
|
||
|
|
my $count = LJ::MemCache::get( $self->num_sold_memkey );
|
||
|
|
return $count if defined $count;
|
||
|
|
|
||
|
|
# check db if not in cache
|
||
|
|
my $dbr = LJ::get_db_reader();
|
||
|
|
$count = $dbr->selectrow_array("SELECT count FROM vgift_counts WHERE vgiftid=$id") || 0;
|
||
|
|
die $dbr->errstr if $dbr->err;
|
||
|
|
|
||
|
|
LJ::MemCache::set( $self->num_sold_memkey, $count, 3600 * 24 );
|
||
|
|
return $count;
|
||
|
|
}
|
||
|
|
|
||
|
|
# 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 num_sold_memkey {
|
||
|
|
my ($self) = @_;
|
||
|
|
return undef unless my $id = $self->id;
|
||
|
|
return [ $id, "vgift.count.$id" ];
|
||
|
|
}
|
||
|
|
|
||
|
|
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 style='margin-top: 0'><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;
|