[dw-free] Invite Code System
[commit: http://hg.dwscoalition.org/dw-free/rev/7de94a158fac]
http://bugs.dwscoalition.org/show_bug.cgi?id=107
More work on the invite code system - distribution end.
Patch by
afuna and
pauamma.
http://bugs.dwscoalition.org/show_bug.cgi?id=107
More work on the invite code system - distribution end.
Patch by
--------------------------------------------------------------------------------
diff -r c4fdb12e764d -r 7de94a158fac bin/upgrading/en.dat
--- a/bin/upgrading/en.dat Fri Feb 20 10:08:15 2009 +0000
+++ b/bin/upgrading/en.dat Fri Feb 20 10:11:42 2009 +0000
@@ -823,7 +823,7 @@ date.month.september.long=September
date.month.september.long=September
date.month.september.short=Sep
-
+
email.invitecoderequest.accept.body<<
Your request for invites has been granted. You can view all your invite codes here:
@@ -835,6 +835,158 @@ email.invitecoderequest.reject.body=Your
email.invitecoderequest.reject.body=Your request for invites has been denied at this time.
email.invitecoderequest.reject.subject=Your invite code request has been denied
+
+email.invitedist.inv.body.html<<
+<html><head>
+<meta http-equiv="Content-Type" content="text/html; charset=[[charset]]">
+</head><body><p>Dear [[username]],</p>
+
+<p>You received [[number]] invite [[?number|code|codes]] as part of a
+distribution to selected users for the following reason:</p>
+
+<blockquote><p>[[reason]]</p></blockquote>
+
+<p>You may use [[?number|that code|those codes]] as you see fit: to create
+secondary accounts for yourself, to invite friends or family, etc. The codes
+are listed below, one per line:</p>
+
+<p>[[codes]]</p>
+
+<p>If you have any question, you can reply to this email.</p>
+
+<p>Sincerely,<br />
+The [[sitename]] Team,<br />
+[[siteroot]]</p></body></html>
+.
+
+email.invitedist.inv.body.plain<<
+Dear [[username]],
+
+You received [[number]] invite [[?number|code|codes]] as part of a distribution
+to selected users for the following reason:
+
+ [[reason]]
+
+You may use [[?number|that code|those codes]] as you see fit: to create
+secondary accounts for yourself, to invite friends or family, etc. The codes
+are listed below, one per line:
+
+[[codes]]
+
+If you have any question, you can reply to this email.
+
+Sincerely,
+The [[sitename]] Team,
+[[siteroot]]
+.
+
+email.invitedist.inv.subject=Invite code distribution
+
+email.invitedist.req.body.adjustdown.html<<
+<p>The number of invites distributed has been adjusted down to [[actinvites]]
+to fit the actual number of eligible users, [[numusers]]. [[remainder]]
+[[?remainder|invite remains|invites remain]] undistributed, and [[peruser]]
+[[?peruser|invite has|invites have]] been issued to each qualifying user.</p>
+.
+
+email.invitedist.req.body.adjustdown.plain<<
+The number of invites distributed has been adjusted down to [[actinvites]]
+to fit the actual number of eligible users, [[numusers]]. [[remainder]]
+[[?remainder|invite remains|invites remain]] undistributed, and [[peruser]]
+[[?peruser|invite has|invites have]] been issued to each qualifying user.
+
+.
+
+email.invitedist.req.body.adjustup.html<<
+<p>The number of invites distributed has been adjusted up to [[actinvites]]
+to fit the actual number of eligible users, [[numusers]]. [[additional]]
+[[?additional|invite has|invites have]] been added, and [[peruser]]
+[[?peruser|invite has|invites have]] been issued to each qualifying user.</p>
+
+.
+
+email.invitedist.req.body.adjustup.plain<<
+The number of invites distributed has been adjusted up to [[actinvites]]
+to fit the actual number of eligible users, [[numusers]]. [[additional]]
+[[?additional|invite has|invites have]] been added, and [[peruser]]
+[[?peruser|invite has|invites have]] been issued to each qualifying user.
+
+.
+
+email.invitedist.req.body.cantadjust.html<<
+<p>Business rules prevent adjusting the number of invites to the actual number
+of eligible users, [[numusers]]. No invites have been distributed.</p>
+.
+
+email.invitedist.req.body.cantadjust.plain<<
+Business rules prevent adjusting the number of invites to the actual number
+of eligible users, [[numusers]]. No invites have been distributed.
+
+.
+
+email.invitedist.req.body.keptsame.html<<
+<p>No adjustment was required to fit the number of invites requested to the
+[[numusers]] eligible [[?numusers|user|users]]. It was kept exactly as you
+requested, and [[peruser]] [[?peruser|invite has|invites have]] been issued to
+each qualifying user.</p>
+.
+
+email.invitedist.req.body.keptsame.plain<<
+No adjustment was required to fit the number of invites requested to the
+[[numusers]] eligible [[?numusers|user|users]]. It was kept exactly as you
+requested, and [[peruser]] [[?peruser|invite has|invites have]] been issued to
+each qualifying user.
+
+.
+
+email.invitedist.req.body.nousers.html<<
+<p>There are no eligible users. No invites have been distributed.</p>
+.
+
+email.invitedist.req.body.nousers.plain<<
+There are no eligible users. No invites have been distributed.
+
+.
+
+email.invitedist.req.body.toomanyusers.html<<
+<p>There are at least [[maxusers]] eligible users, which is more than the
+number of invites to distribute, even after the maximum adjustment upward
+allowed by business rules. No invites have been distributed.</p>
+.
+
+email.invitedist.req.body.toomanyusers.plain<<
+There are at least [[maxusers]] eligible users, which is more than the
+number of invites to distribute, even after the maximum adjustment upward
+allowed by business rules. No invites have been distributed.
+
+.
+
+email.invitedist.req.footer.html<<
+<p>Sincerely,<br />
+The [[sitename]] team,<br />
+[[siteroot]]</p></body></html>
+.
+
+email.invitedist.req.footer.plain<<
+Sincerely,
+The [[sitename]] team,
+[[siteroot]]
+.
+
+email.invitedist.req.header.html<<
+<html><head>
+<meta http-equiv="Content-Type" content="text/html; charset=[[charset]]">
+</head><body>
+<p>You asked to distribute [[numinvites]] [[?numinvite|code|codes]] among users in class "[[class]]".</p>
+.
+
+email.invitedist.req.header.plain<<
+You asked to distribute [[numinvites]] invite [[?numinvite|code|codes]]
+among users in class "[[class]]".
+
+.
+
+email.invitedist.req.subject=Result of your invite code distribution request
email.newacct.body<<
Congratulations, you have a new [[sitename]] account!
@@ -1666,6 +1818,8 @@ error.noentry=No such journal entry.
error.noentry=No such journal entry.
error.nojournal=Unknown Journal
+
+error.noschwartz=TheSchwartz not installed or not configured properly.
error.notloggedin=You have to <a [[aopts]]>log in</a> in order to use this page.
diff -r c4fdb12e764d -r 7de94a158fac bin/worker/distribute-invites
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/worker/distribute-invites Fri Feb 20 10:11:42 2009 +0000
@@ -0,0 +1,26 @@
+#!/usr/bin/perl
+#
+# bin/worker/distribute-invites
+#
+# TheSchwartz worker for invite code distribution.
+#
+# Authors:
+# Pau Amma <pauamma@cpan.org>
+#
+# Copyright (c) 2009 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself. For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+
+use strict;
+use warnings;
+
+use lib "$ENV{LJHOME}/cgi-bin";
+use LJ::Worker::TheSchwartz;
+use DW::Worker::DistributeInvites;
+
+schwartz_decl( $_ )
+ foreach (DW::Worker::DistributeInvites->schwartz_capabilities);
+
+schwartz_work(); # Never returns.
diff -r c4fdb12e764d -r 7de94a158fac cgi-bin/DW/BusinessRules/InviteCodes.pm
--- a/cgi-bin/DW/BusinessRules/InviteCodes.pm Fri Feb 20 10:08:15 2009 +0000
+++ b/cgi-bin/DW/BusinessRules/InviteCodes.pm Fri Feb 20 10:11:42 2009 +0000
@@ -102,6 +102,8 @@ defined as "too many" by max_users(). Th
defined as "too many" by max_users(). This is so the caller can tell "too many"
from "just the number we wanted".
+This function should be called from TheSchwartz only.
+
The default implementation just returns a bunch of random userids for personal
journals (not deleted or suspended) with validated email addresses. Note that
it uses a slow role for its database access. This is a good idea, and your
diff -r c4fdb12e764d -r 7de94a158fac cgi-bin/DW/Worker/DistributeInvites.pm
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cgi-bin/DW/Worker/DistributeInvites.pm Fri Feb 20 10:11:42 2009 +0000
@@ -0,0 +1,195 @@
+#!/usr/bin/perl
+#
+# DW::Worker::DistributeInvites
+#
+# TheSchwartz worker module for invite code distribution. Called with:
+# LJ::theschwartz()->insert('DW::Worker::DistributeInvites',
+# { requester => $remote->userid, searchclass => 'lucky',
+# invites => 42, reason => 'Because I wanna' } );
+#
+# Authors:
+# Pau Amma <pauamma@cpan.org>
+#
+# Copyright (c) 2009 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself. For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+
+use strict;
+use warnings;
+use lib "$LJ::HOME/cgi-bin";
+
+package DW::Worker::DistributeInvites;
+use base 'TheSchwartz::Worker';
+use DW::InviteCodes;
+use DW::BusinessRules::InviteCodes;
+use LJ::User;
+
+BEGIN { require "ljlang.pl"; require "ljmail.pl"; }
+
+sub schwartz_capabilities { return ('DW::Worker::DistributeInvites'); }
+
+sub max_retries { 5 }
+
+sub retry_delay {
+ my ($class, $fails) = @_;
+
+ return (10, 30, 60, 300, 600)[$fails];
+}
+
+sub keep_exit_status_for { 86400 } # 24 hours
+
+# TODO: tune value?
+sub grab_for { 600 }
+
+sub work {
+ my ($class, $job) = @_;
+ my $arg = $job->arg;
+
+ my ($req_uid, $uckey, $ninv, $reason)
+ = map delete $arg->{$_} qw( requester searchclass invites reason );
+
+ return $job->permanent_failure( "Unknown keys: " . join( ", ", keys %$arg ))
+ if keys %$arg;
+ return $job->permanent_failure( "Missing argument" )
+ unless defined $req_uid and defined $uckey
+ and defined $ninv and defined $reason;
+
+ my $class_names = DW::BusinessRules::InviteCodes::user_classes();
+ return $job->permanent_failure( "Unknown user class: $uckey" )
+ unless exists $class_names->{$uckey};
+
+ # Be optimistic and assume failure to load_userid = transient problem
+ my $req_user = LJ::load_userid( $req_uid )
+ or return $job->failed( "Unable to load requesting user" );
+
+ my $max_nusers = DW::BusinessRules::InviteCodes::max_users( $ninv );
+ my $inv_uids
+ = DW::BusinessRules::InviteCodes::search_class( $uckey, $max_nusers );
+ my $inv_nusers = scalar @$inv_uids;
+
+ # Report email for requester
+ my $req_lang = $req_user->prop( 'browselang' ) || $LJ::DEFAULT_LANG;
+ my $req_usehtml = $req_user->prop( 'opt_htmlemail' ) eq 'Y';
+ my $req_charset = $req_user->mailencoding || 'utf-8';
+ my %req_email = (
+ from => $LJ::ACCOUNTS_EMAIL,
+ fromname => $LJ::SITECOMPANY,
+ to => $req_user->email_raw,
+ charset => $req_charset,
+ subject => LJ::Lang::get_text( $req_lang,
+ 'email.invitedist.req.subject', undef, {} ),
+ body => LJ::Lang::get_text( $req_lang, # Gets extended later.
+ 'email.invitedist.req.header.plain', undef,
+ { class => $class_names->{$uckey}, numinvites => $ninv } ) );
+
+ $req_email{html} = LJ::Lang::get_text( $req_lang,
+ 'email.invitedist.req.header.html', undef,
+ { class => $class_names->{$uckey}, numinvites => $ninv,
+ charset => $req_charset } )
+ if $req_usehtml;
+
+ my ($reqemail_body, $reqemail_vars);
+
+ # Figure out what to do, based on the number of invites and users
+ if ( $max_nusers <= $inv_nusers ) {
+ $reqemail_body = 'toomanyusers';
+ $reqemail_vars = { maxusers => $max_nusers };
+ } elsif ( $inv_nusers == 0 ) {
+ $reqemail_body = 'nousers';
+ $reqemail_vars = {};
+ } else {
+ my $adj_ninv
+ = DW::BusinessRules::InviteCodes::adj_invites( $ninv, $inv_nusers );
+
+ if ($adj_ninv == 0) {
+ $reqemail_body = 'cantadjust';
+ $reqemail_vars = { numusers => $inv_nusers };
+ } elsif ( $adj_ninv < $ninv ) {
+ $reqemail_body = 'adjustdown';
+ $reqemail_vars = { actinvites => $adj_ninv,
+ remainder => $ninv - $adj_ninv,
+ numusers => $inv_nusers };
+ } elsif ( $adj_ninv > $ninv ) {
+ $reqemail_body = 'adjustup';
+ $reqemail_vars = { actinvites => $adj_ninv,
+ additional => $adj_ninv - $ninv,
+ numusers => $inv_nusers };
+ } else {
+ $reqemail_body = 'keptsame';
+ $reqemail_vars = { numusers => $inv_nusers };
+ }
+
+ if ( $adj_ninv > 0 ) {
+ # Here, we know we'll be generating invites, so get cracking.
+ my $inv_peruser = int( $adj_ninv / $inv_nusers );
+ $reqemail_vars->{peruser} = $inv_peruser;
+
+ # TODO: make magic number configurable
+ for (my $start = 0; $start < $inv_nusers; $start += 1000) {
+ my $end = ($start + 999 < $inv_nusers)
+ ? $start + 999
+ : $inv_nusers - 1;
+ my $inv_uhash = LJ::load_userids( @{$inv_uids}[$start..$end] );
+ foreach my $inv_user (values %$inv_uhash) {
+ my @ics = DW::InviteCodes->generate( count => $inv_peruser,
+ owner => $inv_user,
+ reason => $reason );
+ my $inv_lang = $inv_user->prop( 'browselang' )
+ || $LJ::DEFAULT_LANG;
+ my $inv_usehtml = $inv_user->prop( 'opt_htmlemail' ) eq 'Y';
+ my $inv_charset = $inv_user->mailencoding || 'utf-8';
+ my $invemail_vars = {
+ username => $inv_user->user, siteroot => $LJ::SITEROOT,
+ sitename => $LJ::SITENAMESHORT, reason => $reason,
+ number => $inv_peruser, codes => join("\n", @ics) };
+
+ my %inv_email = (
+ from => $LJ::ACCOUNTS_EMAIL,
+ fromname => $LJ::SITECOMPANY,
+ to => $inv_user->email_raw,
+ charset => $inv_charset,
+ subject => LJ::Lang::get_text( $inv_lang,
+ 'email.invitedist.inv.subject', undef, {} ),
+ body => LJ::Lang::get_text( $inv_lang,
+ 'email.invitedist.inv.body.plain', undef,
+ $invemail_vars ) );
+
+ $invemail_vars->{codes} = join("<br />\n", @ics);
+ $invemail_vars->{charset} = $inv_charset;
+ $inv_email{html} = LJ::Lang::get_text( $inv_lang,
+ 'email.invitedist.inv.body.html', undef,
+ $invemail_vars )
+ if $inv_usehtml;
+
+ LJ::send_mail( \%inv_email )
+ or $job->debug( "Can't email " . $inv_user->user );
+ }
+ }
+ }
+ }
+
+ $req_email{body} .= LJ::Lang::get_text( $req_lang,
+ "email.invitedist.req.body.${reqemail_body}.plain",
+ undef, $reqemail_vars );
+ $req_email{body} .= LJ::Lang::get_text( $req_lang,
+ 'email.invitedist.req.footer.plain', undef,
+ { sitename => $LJ::SITENAMESHORT, siteroot => $LJ::SITEROOT } );
+
+ if ($req_usehtml) {
+ $req_email{html} .= LJ::Lang::get_text( $req_lang,
+ "email.invitedist.req.body.${reqemail_body}.html",
+ undef, $reqemail_vars );
+ $req_email{html} .= LJ::Lang::get_text( $req_lang,
+ 'email.invitedist.req.footer.html', undef,
+ { sitename => $LJ::SITENAMESHORT, siteroot => $LJ::SITEROOT } );
+ }
+
+ LJ::send_mail( \%req_email )
+ or $job->debug( "Can't email requester" );
+
+ $job->completed;
+}
+
+1;
diff -r c4fdb12e764d -r 7de94a158fac htdocs/admin/invites/distribute.bml
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/admin/invites/distribute.bml Fri Feb 20 10:11:42 2009 +0000
@@ -0,0 +1,65 @@
+<?page
+body<=
+<?_code
+{
+ # Admin page to generate and distribute from a pool of invites.
+ #
+ # Authors:
+ # Afuna <coder.dw@afunamatata.com>
+ #
+ # Copyright (c) 2009 by Dreamwidth Studios, LLC.
+ #
+ # This program is free software; you may redistribute it and/or modify it
+ # under the same terms as Perl itself. For a copy of the license, please
+ # reference 'perldoc perlartistic' or 'perldoc perlgpl'.
+
+ use strict;
+ use vars qw(%POST);
+
+ my $remote = LJ::get_remote();
+ return LJ::error_noremote() unless $remote;
+
+ return BML::redirect( $LJ::SITEROOT )
+ unless LJ::check_priv( $remote, 'payments' );
+
+ my $ret;
+
+ my $classes = DW::BusinessRules::InviteCodes::user_classes();
+
+ $ret .= '<form method="POST">';
+ $ret .= LJ::labelfy( 'num_invites', "$ML{'.field.numinvites.label'} " );
+ $ret .= LJ::html_text( { id => 'num_invites', name => 'num_invites' } ) . '<br />';
+ $ret .= LJ::labelfy( 'user_class', "$ML{'.field.distribute.label'} ");
+ $ret .= LJ::html_select( { id => 'user_class', name => 'user_class' }, %$classes ) . '<br />';
+ $ret .= LJ::labelfy( 'reason', "$ML{'.field.reason.label'} " );
+ $ret .= LJ::html_text( { id => 'reason', name => 'reason', maxlength => 255 } ) . '<br />';
+ $ret .= LJ::html_submit( value => $ML{'.btn.distribute'} );
+ $ret .= LJ::form_auth;
+ $ret .= '</form>';
+
+ if ( LJ::did_post ) {
+ return LJ::error_list( $ML{'error.invalidform'} )
+ unless LJ::check_form_auth;
+
+ my $num_invites_requested = $POST{num_invites};
+ my $selected_user_class = $POST{user_class};
+ my $reason = $POST{reason};
+
+ my $sclient = LJ::theschwartz()
+ or return LJ::error_list( $ML{'error.noschwartz'} );
+ $sclient->insert('DW::Worker::DistributeInvites',
+ { requester => $remote->userid,
+ searchclass => $selected_user_class,
+ invites => $num_invites_requested,
+ reason => $reason } )
+ or return LJ::error_list( $ML{'.error.cantinsertjob'} );
+
+ $ret .= "<?hr?>";
+ $ret .= $ML{'.success.jobstarted'};
+ }
+ return $ret;
+}
+_code?>
+<=body
+title=> <?_code return $ML{'.title'}; _code?>
+page?>
diff -r c4fdb12e764d -r 7de94a158fac htdocs/admin/invites/distribute.bml.text
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/admin/invites/distribute.bml.text Fri Feb 20 10:11:42 2009 +0000
@@ -0,0 +1,13 @@
+.btn.distribute=Distribute
+
+.error.cantinsertjob=Unable to start TheSchwartz job for invite codes distribution.
+
+.field.distribute.label=Distribute to:
+
+.field.numinvites.label=Number of invites:
+
+.field.reason.label=Reason for invites:
+
+.success.jobstarted=Invite code distribution successfully initiated. You will receive a report by email to your registered address when it is complete.
+
+.title=Distribute Invite Codes
--------------------------------------------------------------------------------
