[dw-free] Add extra icon purchasing
[commit: http://hg.dwscoalition.org/dw-free/rev/1ead40895ac9]
Add extra icon purchasing
This lets users purchase permanent extra icons for their accounts. They cost
$1 USD (or 10 points) each and never expire. This also includes a console
tool for admins to maintain them.
This does not yet support transferring between accounts. We will do that
soon.
Patch by
mark.
Files modified:
Add extra icon purchasing
This lets users purchase permanent extra icons for their accounts. They cost
$1 USD (or 10 points) each and never expire. This also includes a console
tool for admins to maintain them.
This does not yet support transferring between accounts. We will do that
soon.
Patch by
![[staff profile]](https://www.dreamwidth.org/img/silk/identity/user_staff.png)
Files modified:
- bin/upgrading/en.dat
- bin/upgrading/proplists.dat
- cgi-bin/DW/Console/Command/BonusIcons.pm
- cgi-bin/DW/Controller/Shop.pm
- cgi-bin/DW/Shop.pm
- cgi-bin/DW/Shop/Item/Icons.pm
- cgi-bin/LJ/Global/Defaults.pm
- cgi-bin/LJ/User.pm
- cvs/multicvs.conf
- views/shop/icons.tt
- views/shop/icons.tt.text
- views/shop/index.tt
- views/shop/index.tt.text
-------------------------------------------------------------------------------- diff -r db1baaa9ddac -r 1ead40895ac9 bin/upgrading/en.dat --- a/bin/upgrading/en.dat Thu Jul 19 21:58:03 2012 -0700 +++ b/bin/upgrading/en.dat Fri Jul 20 17:49:33 2012 -0700 @@ -3250,7 +3250,7 @@ [[gift]] - +[[extra]] Regards, The [[sitename]] Team . @@ -3264,7 +3264,7 @@ [[gift]] - +[[extra]] Regards, The [[sitename]] Team . @@ -3572,6 +3572,29 @@ shop.item.account.randomuser=Random active free user +shop.item.icons.canbeadded.banned=You are restricted from making purchases for this journal. + +shop.item.icons.canbeadded.invalidjournaltype=You can only buy icons for a personal journal. + +shop.item.icons.canbeadded.itemerror=Could not add item to cart. + +shop.item.icons.canbeadded.notauser=You can only buy icons for an active account. + +shop.item.icons.canbeadded.notpaid=That account is ineligible for extra icons. These can only be purchased for a paid account. + +shop.item.icons.canbeadded.outofrange=You can only buy up to [[count]] icons for this journal. + +shop.item.icons.name=[[num]] [[sitename]] icons + +shop.item.icons.overlimit<< +NOTE: This order has pushed your account over the [[sitename]] limit +of [[max]] icons. If you would like to transfer the surplus icons to +another account, or convert them into [[sitename]] Points, please +contact us for assistance. + + +. + shop.item.points.canbeadded.banned=You are restricted from making purchases for this journal. shop.item.points.canbeadded.insufficient=You do not have enough points to do that. diff -r db1baaa9ddac -r 1ead40895ac9 bin/upgrading/proplists.dat --- a/bin/upgrading/proplists.dat Thu Jul 19 21:58:03 2012 -0700 +++ b/bin/upgrading/proplists.dat Fri Jul 20 17:49:33 2012 -0700 @@ -46,6 +46,14 @@ multihomed: 0 prettyname: Beta Features List +userproplist.bonus_icons: + cldversion: 0 + datatype: num + des: Count of permanent extra icon slots + indexed: 0 + multihomed: 0 + prettyname: How many bonus icon slots the user has purchased + userproplist.browselang: cldversion: 4 datatype: char diff -r db1baaa9ddac -r 1ead40895ac9 cgi-bin/DW/Console/Command/BonusIcons.pm --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cgi-bin/DW/Console/Command/BonusIcons.pm Fri Jul 20 17:49:33 2012 -0700 @@ -0,0 +1,81 @@ +#!/usr/bin/perl +# +# DW::Console::Command::BonusIcons +# +# Console commands for managing bonus icons. +# +# Authors: +# Mark Smith <mark@dreamwidth.org> +# +# Copyright (c) 2012 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::Console::Command::BonusIcons; +use strict; + +use base qw/ LJ::Console::Command /; +use Carp qw/ croak /; +use List::Util qw/ max /; + +sub cmd { 'bonus_icons' } +sub desc { 'Manage bonus icons for an account.' } +sub args_desc { + [ + 'command' => 'Subcommand: add, remove.', + 'username' => 'Username to act on.', + 'count' => 'How many icons to add or remove.', + ] +} +sub usage { '<username> [<subcommand> <count>]' } +sub can_execute { 1 } + +sub execute { + my ( $self, $user, $cmd, $count ) = @_; + + my $remote = LJ::get_remote(); + return $self->error( 'You must be logged in!' ) + unless $remote; + return $self->error( 'I\'m afraid I can\'t let you do that.' ) + unless $remote->has_priv( 'payments' => 'bonus_icons' ); + + my $to_u = LJ::load_user( $user ); + return $self->error( 'Invalid user.' ) + unless $to_u; + + unless ( defined $cmd ) { + # No subcommand to add or remove. Just print how many icons they have. + return $self->print( sprintf( '%s has %d bonus icons.', + $to_u->user, $to_u->prop( 'bonus_icons' ) ) ); + } + + return $self->error( 'Invalid subcommand.' ) + if $cmd && $cmd !~ /^(?:add|remove)$/; + + return $self->error( 'Count must be a positive integer.' ) + unless $count =~ /^\d+$/; + $count += 0; + + if ( $cmd eq 'add' ) { + my $new = max( $to_u->prop( 'bonus_icons' ) + $count, 0 ); + $to_u->set_prop( bonus_icons => $new ); + LJ::statushistory_add( $to_u, $remote, 'bonus_icons', + sprintf( 'Added %d icons, new total: %d.', $count, $new ) ); + $self->print( sprintf( 'User now has %d icons.', $new ) ); + + } elsif ( $cmd eq 'remove' ) { + my $new = max( $to_u->prop( 'bonus_icons' ) - $count, 0 ); + $to_u->set_prop( bonus_icons => $new ); + LJ::statushistory_add( $to_u, $remote, 'bonus_icons', + sprintf( 'Removed %d icons, new total: %d.', $count, $new ) ); + $self->print( sprintf( 'User now has %d icons.', $new ) ); + + } + + return 1; +} + +1; diff -r db1baaa9ddac -r 1ead40895ac9 cgi-bin/DW/Controller/Shop.pm --- a/cgi-bin/DW/Controller/Shop.pm Thu Jul 19 21:58:03 2012 -0700 +++ b/cgi-bin/DW/Controller/Shop.pm Fri Jul 20 17:49:33 2012 -0700 @@ -27,6 +27,7 @@ # routing directions DW::Routing->register_string( '/shop', \&shop_index_handler, app => 1 ); DW::Routing->register_string( '/shop/points', \&shop_points_handler, app => 1 ); +DW::Routing->register_string( '/shop/icons', \&shop_icons_handler, app => 1 ); DW::Routing->register_string( '/shop/transferpoints', \&shop_transfer_points_handler, app => 1 ); # our basic shop controller, this does setup that is unique to all shop @@ -238,5 +239,62 @@ return DW::Template->render_template( 'shop/points.tt', $rv ); } +# handles the shop buy icons page +sub shop_icons_handler { + my ( $ok, $rv ) = _shop_controller(); + return $rv unless $ok; + + my $remote = $rv->{remote}; + my %errs; + $rv->{errs} = \%errs; + + my $r = DW::Request->get; + if ( $r->did_post ) { + my $args = $r->post_args; + die "invalid auth\n" unless LJ::check_form_auth( $args->{lj_form_auth} ); + + my $u = LJ::load_user( $args->{foruser} ); + my $icons = int( $args->{icons} + 0 ); + my $item; # provisionally create the item to access object methods + + if ( !$u ) { + $errs{foruser} = LJ::Lang::ml( 'shop.item.icons.canbeadded.notauser' ); + + } elsif ( $item = DW::Shop::Item::Icons->new( target_userid => $u->id, from_userid => $remote->id, icons => $icons ) ) { + # error check the user + if ( $item->can_be_added_user( errref => \$errs{foruser} ) ) { + $rv->{foru} = $u; + delete $errs{foruser}; # undefined + } + + # error check the icons + if ( $item->can_be_added_icons( errref => \$errs{icons} ) ) { + $rv->{icons} = $icons; + delete $errs{icons}; # undefined + } + + } else { + $errs{foruser} = LJ::Lang::ml( 'shop.item.icons.canbeadded.itemerror' ); + } + + # looks good, add it! + unless ( keys %errs ) { + $rv->{cart}->add_item( $item ); + return $r->redirect( "$LJ::SITEROOT/shop" ); + } + + } else { + my $for = $r->get_args->{for}; + + if ( ! $for || $for eq 'self' ) { + $rv->{foru} = $remote; + } else { + $rv->{foru} = LJ::load_user( $for ); + } + } + + return DW::Template->render_template( 'shop/icons.tt', $rv ); +} + 1; diff -r db1baaa9ddac -r 1ead40895ac9 cgi-bin/DW/Shop.pm --- a/cgi-bin/DW/Shop.pm Thu Jul 19 21:58:03 2012 -0700 +++ b/cgi-bin/DW/Shop.pm Fri Jul 20 17:49:33 2012 -0700 @@ -25,6 +25,7 @@ use DW::Shop::Item::Account; use DW::Shop::Item::Points; use DW::Shop::Item::Rename; +use DW::Shop::Item::Icons; # constants across the site our $MIN_ORDER_COST = 3.00; # cost in USD minimum. this only comes into affect if @@ -189,5 +190,4 @@ or confess 'tried to get shop without calling DW::Shop->initialize()'; } - 1; diff -r db1baaa9ddac -r 1ead40895ac9 cgi-bin/DW/Shop/Item/Icons.pm --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cgi-bin/DW/Shop/Item/Icons.pm Fri Jul 20 17:49:33 2012 -0700 @@ -0,0 +1,230 @@ +#!/usr/bin/perl +# +# DW::Shop::Item::Icons +# +# Represents Dreamwidth Icons that someone is buying. +# +# Authors: +# Mark Smith <mark@dreamwidth.org> +# +# Copyright (c) 2012 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::Shop::Item::Icons; + +use base 'DW::Shop::Item'; + +use strict; +use DW::InviteCodes; +use DW::Pay; + +=head1 NAME + +DW::Shop::Item::Icons - Represents extra icons that someone is purchasing. See +the documentation for DW::Shop::Item for usage examples and description of methods +inherited from that base class. + +=head1 API + +=head2 C<< $class->new( [ %args ] ) >> + +Instantiates a block of icons to be purchased. + +Arguments: +=item ( see DW::Shop::Item ), +=item icons => number of icons to buy, + +=cut + +# override +sub new { + my ( $class, %args ) = @_; + + my $self = $class->SUPER::new( %args, type => 'icons' ); + return unless $self; + + # Set up our initial cost structure + $self->{cost_cash} = $self->{icons}; + $self->{cost_points} = 0; + + # for now, we can only apply to a user. in the future this is an obvious way + # to do gift certificates by allowing an email address here... + die "Can only give icons to an account.\n" + unless $self->t_userid; + + return $self; +} + + +# override +sub _apply { + my $self = $_[0]; + + return $self->_apply_userid if $self->t_userid; + + # something weird, just kill this item! + $self->{applied} = 1; + return 1; +} + + +# internal application sub, do not call +sub _apply_userid { + my $self = $_[0]; + return 1 if $self->applied; + + # will need this later + my $fu = LJ::load_userid( $self->from_userid ); + unless ( $fu ) { + warn "Failed to apply: invalid from_userid!\n"; + return 0; + } + + # need this user + my $u = LJ::load_userid( $self->t_userid ) + or return 0; + + # validate that they can get this number of icons + my $cur = $u->prop( 'bonus_icons' ) // 0; + $u->set_prop( bonus_icons => $cur + $self->icons ); + LJ::statushistory_add( $u, $fu, 'bonus_icons', + sprintf( '%d icons added; item #%d', $self->icons, $self->id ) ); + + # we're applied now, regardless of what happens with the email + $self->{applied} = 1; + + # see if this has put the user over their limit + my $overlimit = ''; + my $real_total = $self->icons + $u->get_cap( 'userpics' ) + $cur; + if ( $real_total > $LJ::USERPIC_MAXIMUM ) { + $overlimit = LJ::Lang::ml( 'shop.item.icons.overlimit', + { sitename => $LJ::SITENAMESHORT, max => $LJ::USERPIC_MAXIMUM, + overage => $real_total - $LJ::USERPIC_MAXIMUM } ); + } + + # now we have to mail this notification + my $word = $fu->equals( $u ) ? 'self' : 'other'; + my $body = LJ::Lang::ml( "shop.email.gift.$word.body", + { + touser => $u->display_name, + fromuser => $fu->display_name, + sitename => $LJ::SITENAME, + gift => sprintf( '%d %s Extra Icons', $self->icons, $LJ::SITENAMESHORT ), + extra => $overlimit, + } + ); + my $subj = LJ::Lang::ml( "shop.email.gift.$word.subject", { sitename => $LJ::SITENAME } ); + + # send the email to the user + LJ::send_mail( { + to => $u->email_raw, + from => $LJ::ACCOUNTS_EMAIL, + fromname => $LJ::SITENAME, + subject => $subj, + body => $body + } ); + + # tell the caller we're happy + return 1; +} + + +# override +sub unapply { + my $self = $_[0]; + return unless $self->applied; + + # unapplying is not coded yet, as we don't have good automatic support for orders being + # reverted and refunded. + $self->{applied} = 0; + die "Unable to unapply right now.\n"; + + return 1; +} + + +# override +sub can_be_added { + my ( $self, %opts ) = @_; + + return 0 unless $self->can_be_added_user( %opts ); + return 0 unless $self->can_be_added_icons( %opts ); + + return 1; +} + + +sub can_be_added_user { + my ( $self, %opts ) = @_; + my $errref = $opts{errref}; + + # if not a valid account, error + my $target_u = LJ::load_userid( $self->t_userid ); + if ( ! LJ::isu( $target_u ) ) { + $$errref = LJ::Lang::ml( 'shop.item.icons.canbeadded.notauser' ); + return 0; + } + + # the receiving user must be a person for now + unless ( $target_u->is_personal && $target_u->is_visible ) { + $$errref = LJ::Lang::ml( 'shop.item.icons.canbeadded.invalidjournaltype' ); + return 0; + } + + # and they must be paid + unless ( $target_u->can_buy_icons ) { + $$errref = LJ::Lang::ml( 'shop.item.icons.canbeadded.notpaid' ); + return 0; + } + + # make sure no sysban is in effect here + my $fromu = LJ::load_userid( $self->from_userid ); + if ( $fromu && $target_u->has_banned( $fromu ) ) { + $$errref = LJ::Lang::ml( 'shop.item.icons.canbeadded.banned' ); + return 0; + } + + return 1; +} + + +sub can_be_added_icons { + my ( $self, %opts ) = @_; + my $errref = $opts{errref}; + + # sanity check that the icons to add are within range + my $target_u = LJ::load_userid( $self->t_userid ); + my $pics_left = $LJ::USERPIC_MAXIMUM - $target_u->userpic_quota; + unless ( $self->icons > 0 && $self->icons <= $pics_left ) { + $$errref = LJ::Lang::ml( 'shop.item.icons.canbeadded.outofrange', + { count => $pics_left }); + return 0; + } + + return 1; +} + + +# override +sub name_text { + my $self = $_[0]; + + return LJ::Lang::ml( 'shop.item.icons.name', + { num => $self->icons, sitename => $LJ::SITENAMESHORT } ); +} + + +=head2 C<< $self->icons >> + +Return how many icons this item is worth. + +=cut + +sub icons { $_[0]->{icons} } + + +1; diff -r db1baaa9ddac -r 1ead40895ac9 cgi-bin/LJ/Global/Defaults.pm --- a/cgi-bin/LJ/Global/Defaults.pm Thu Jul 19 21:58:03 2012 -0700 +++ b/cgi-bin/LJ/Global/Defaults.pm Fri Jul 20 17:49:33 2012 -0700 @@ -371,6 +371,9 @@ # Secrets %SECRETS = () unless defined %SECRETS; + + # Userpic maximum. No user can have more than this. + $USERPIC_MAXIMUM ||= 500; } diff -r db1baaa9ddac -r 1ead40895ac9 cgi-bin/LJ/User.pm --- a/cgi-bin/LJ/User.pm Thu Jul 19 21:58:03 2012 -0700 +++ b/cgi-bin/LJ/User.pm Fri Jul 20 17:49:33 2012 -0700 @@ -1,3 +1,6 @@ +# +# NOTE: This module now requires Perl 5.10 or greater. +# # This code was forked from the LiveJournal project owned and operated # by Live Journal, Inc. The code has been modified and expanded by # Dreamwidth Studios, LLC. These files were originally licensed under @@ -32,8 +35,8 @@ package LJ::User; use Carp; use Storable; +use List::Util qw/ min /; use lib "$LJ::HOME/cgi-bin"; -use List::Util (); use LJ::Global::Constants; use LJ::MemCache; use LJ::Session; @@ -1946,6 +1949,10 @@ return $_[0]->get_cap( 'beta_payments' ) ? 1 : 0; } +sub can_buy_icons { + return $_[0]->get_cap( 'bonus_icons' ) ? 1 : 0; +} + sub can_create_feeds { return $_[0]->get_cap( 'synd_create' ) ? 1 : 0; } @@ -2324,7 +2331,7 @@ } sub count_max_userpics { - return $_[0]->get_cap( 'userpics' ); + return $_[0]->userpic_quota; } sub count_max_xpost_accounts { @@ -3053,7 +3060,7 @@ =cut sub shop_points { - return $_[0]->prop( 'shop_points' )+0; + return $_[0]->prop( 'shop_points' ) // 0; } @@ -6412,7 +6419,6 @@ # active / inactive lists my @active = (); my @inactive = (); - my $allow = LJ::get_cap($u, "userpics"); # get a database handle for reading/writing my $dbh = LJ::get_db_writer(); @@ -6432,8 +6438,9 @@ } # inactivate previously activated userpics - if (@active > $allow) { - my $to_ban = @active - $allow; + my $allowed = $u->userpic_quota; + if (scalar @active > $allowed) { + my $to_ban = scalar @active - $allowed; # find first jitemid greater than time 2 months ago using rlogtime index # ($LJ::EndOfTime - UnixTime) @@ -6491,8 +6498,8 @@ } # activate previously inactivated userpics - if (@inactive && @active < $allow) { - my $to_activate = $allow - @active; + if (scalar @inactive && scalar @active < $allowed) { + my $to_activate = $allowed - @active; $to_activate = @inactive if $to_activate > @inactive; # take the $to_activate newest (highest numbered) pictures @@ -7217,17 +7224,17 @@ return $_[0]->dversion >= 9; } - =head3 C<< $u->userpic_quota >> -Returns the number of userpics the user can upload +Returns the number of userpics the user can upload (base account type cap + bonus slots purchased) =cut sub userpic_quota { my $u = shift or return undef; - my $quota = $u->get_cap('userpics'); - - return $quota; + my $ct = $u->get_cap( 'userpics' ); + $ct += $u->prop('bonus_icons') // 0 + if $u->is_paid; # paid accounts get bonus icons + return min( $ct, $LJ::USERPIC_MAXIMUM ); } # Intentionally no POD here. diff -r db1baaa9ddac -r 1ead40895ac9 cvs/multicvs.conf --- a/cvs/multicvs.conf Thu Jul 19 21:58:03 2012 -0700 +++ b/cvs/multicvs.conf Fri Jul 20 17:49:33 2012 -0700 @@ -11,7 +11,7 @@ CVSDIR=$LJHOME/cvs # DreamWidth repositories -HG(dw-free) = http://hg.dwscoalition.org/dw-free @stable +#HG(dw-free) = http://hg.dwscoalition.org/dw-free @stable # stock/unchanged repositories pulled from external sources SVN(gearman) = http://code.livejournal.org/svn/gearman/trunk/ diff -r db1baaa9ddac -r 1ead40895ac9 views/shop/icons.tt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/views/shop/icons.tt Fri Jul 20 17:49:33 2012 -0700 @@ -0,0 +1,38 @@ +[%- sections.title = '.title' | ml(sitename = site.nameshort) -%] + +[% cart_display %] + +<p>[% '.about' | ml(sitename = site.nameshort) %]</p> + +<form method='post'> +[% dw.form_auth %] +<table summary='' class='shop-table-gift'> +[% IF foru %] + <tr><td>[% '.buying.for' | ml %]</td><td>[% foru.ljuser_display %] + <input type='hidden' name='foruser' value='[% foru.user %]' /> + <input type='hidden' name='maxicons' value='[% maxicons %]' /> + </td></tr> +[% ELSE %] + <tr><td>[% '.buying.for' | ml %]</td><td><input type='text' name='foruser' maxlength='25' size='15' /> + [% IF errs.foruser %]<br /><strong>[% errs.foruser %][% END %] + </td></tr> +[% END %] +<tr><td>[% '.buying.icons' | ml %]</td> + <td><input type='text' name='icons' id='icons' maxlength='3' size='10' value='[% icons %]' /> + [% IF errs.icons %]<br /><strong>[% errs.icons %][% END %] +</td></tr> +<tr><td><span id='icons-cost'></span></td><td><input type='submit' value='[% '.addtocart' | ml %]' /></td></tr> +</table> +</form> + +<p id='icons-about'>[% '.about2' | ml %]</p> + +[%# FIXME: move this to shop.js or something %] +<script type='text/javascript'> + jQuery( function($) { + setInterval( + function() { + $('#icons-cost').html( 'Cost: <strong>$' + ($('#icons').val() / 1).toFixed(2) + ' USD</strong>' ); + }, 250 ); + } ); +</script> diff -r db1baaa9ddac -r 1ead40895ac9 views/shop/icons.tt.text --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/views/shop/icons.tt.text Fri Jul 20 17:49:33 2012 -0700 @@ -0,0 +1,13 @@ +;; -*- coding: utf-8 -*- +.about=This page allows you to buy extra icons for a [[sitename]] account. + +.about2=Extra icons purchased for an account never expire. Each extra icon you purchase will cost $1.00 USD. + +.addtocart=Add To Cart + +.buying.for=Buying for Account: + +.buying.icons=Extra Icons to Purchase: + +.title=[[sitename]] Extra Icons Shop + diff -r db1baaa9ddac -r 1ead40895ac9 views/shop/index.tt --- a/views/shop/index.tt Thu Jul 19 21:58:03 2012 -0700 +++ b/views/shop/index.tt Fri Jul 20 17:49:33 2012 -0700 @@ -45,6 +45,19 @@ </div> </div> +<div class='shop-category'> + <div class='shop-category-title'>[% '.title.icons' | ml %]</div> + <div class='shop-category-items'> +[% IF remote AND remote.is_personal %] + <span class='shop-category-item'><a href="[% site.root %]/shop/icons?for=self">[% '.for.self' | ml %]</a> ([% remote.ljuser_display %])</span> +[% END %] +[% IF remote AND remote.is_personal %] + <span class='shop-category-item'><a href="[% site.root %]/shop/icons?for=gift">[% '.for.different' | ml %]</a></span> +[% ELSE %] + <span class='shop-category-item'><a href="[% site.root %]/shop/icons?for=gift">[% '.for.existing' | ml %]</a></span> +[% END %] + </div> +</div> [%# Here for future expansion ... diff -r db1baaa9ddac -r 1ead40895ac9 views/shop/index.tt.text --- a/views/shop/index.tt.text Thu Jul 19 21:58:03 2012 -0700 +++ b/views/shop/index.tt.text Fri Jul 20 17:49:33 2012 -0700 @@ -19,6 +19,8 @@ .title=[[sitename]] Shop +.title.icons=Buy extra icons for... + .title.paidacc=Buy paid services for... .title.points=Buy [[site]] Points for... --------------------------------------------------------------------------------