mark: A photo of Mark kneeling on top of the Taal Volcano in the Philippines. It was a long hike. (Default)
Mark Smith ([staff profile] mark) wrote in [site community profile] changelog2009-02-20 10:11 am

[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 [personal profile] afuna and [personal profile] pauamma.

--------------------------------------------------------------------------------
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
--------------------------------------------------------------------------------