[Jifty-commit] r4478 - in apps/spensive: . bin doc etc lib/Spensive lib/Spensive/Action lib/Spensive/Notification lib/Spensive/Notification/EmailError

jifty-commit at lists.jifty.org jifty-commit at lists.jifty.org
Tue Nov 20 12:10:06 EST 2007


Author: jesse
Date: Tue Nov 20 12:10:05 2007
New Revision: 4478

Added:
   apps/spensive/bin/mailgate   (contents, props changed)
   apps/spensive/doc/TODO
   apps/spensive/lib/Spensive/Action/EmailDispatch.pm   (contents, props changed)
   apps/spensive/lib/Spensive/Notification/
   apps/spensive/lib/Spensive/Notification/EmailError/
   apps/spensive/lib/Spensive/Notification/EmailError.pm   (contents, props changed)
Modified:
   apps/spensive/   (props changed)
   apps/spensive/etc/config.yml
   apps/spensive/lib/Spensive/Model/Attachment.pm
   apps/spensive/lib/Spensive/Model/Expense.pm
   apps/spensive/lib/Spensive/Model/User.pm
   apps/spensive/lib/Spensive/View.pm

Log:
 r67115 at pinglin (orig r7292):  jesse | 2007-09-10 22:42:47 -0400
 Merging!


Added: apps/spensive/bin/mailgate
==============================================================================
--- (empty file)
+++ apps/spensive/bin/mailgate	Tue Nov 20 12:10:05 2007
@@ -0,0 +1,81 @@
+#!/usr/bin/env perl
+
+use strict;
+use warnings;
+use Getopt::Long;
+use LWP::UserAgent;
+use Pod::Usage;
+use Jifty::YAML;
+
+# Option parsing
+my %opts;
+GetOptions( \%opts, "address=s", "url=s", "timeout=i", "sender=s", "help" )
+  or pod2usage( -exitval => 1, -verbose => 1 );
+pod2usage( -exitval => 1, -verbose => 1 ) if $opts{help} or not $opts{url};
+
+# Set up POST parameters
+my %args = ( "J:A-dispatch" => "EmailDispatch" );
+
+# Where the email was sent to
+$args{'J:A:F-address-dispatch'} = $opts{address} || $ENV{RECIPIENT};
+die "$0: No --address argument, and EXTENSION env variable is empty!"
+  unless $args{'J:A:F-address-dispatch'};
+
+# Who sent it?
+$args{'J:A:F-envelope_sender-dispatch'} = $opts{sender} || $ENV{SENDER};
+die "$0: No --sender argument, and SENDER env variable is empty!"
+  unless $args{'J:A:F-envelope_sender-dispatch'};
+
+# Grab the message from STDIN
+$args{'J:A:F-email-dispatch'} = do { local (@ARGV, $/); <> };
+die "$0: No message passed on STDIN\n"
+  unless $args{'J:A:F-email-dispatch'} =~ /\S/;
+
+# Send 'er away!
+my $ua = LWP::UserAgent->new();
+$ua->timeout( $opts{timeout} ) if defined $opts{timeout};
+my $r = $ua->post("$opts{url}/__jifty/webservices/yaml", { %args });
+if ($r->is_success) {
+    my $data = Jifty::YAML::Load($r->content);
+    exit( $data->{dispatch}{failure} ? 100 : 0 );
+} else {
+    warn "Failure to post";
+    exit 1;
+}
+
+=head1 SYNOPSIS
+
+    mailgate --help       # This text
+
+    mailgate --url http://hiveminder.com/
+
+=head1 OPTIONS
+
+=over 3
+
+=item --url
+
+The only required parameter; this specifies the complete base URL to
+the Hiveminder server that the mail is destined for.
+
+=item --address
+
+Specifies the published address that the mail is destined for.  If
+this option is omitted, C<mailgate> looks at the C<EXTENSION>
+environment variable.  The address determines what will be done with
+the email when it is received.
+
+=item --timeout
+
+Sets the timeout before the connection aborts.  The default is three
+minutes, which should be more than sufficient.
+
+=item --sender
+
+Sets the envelope sender of the message. This will be pulled from the
+C<SENDER> environment variable if not specified.
+
+=back
+
+=cut
+

Added: apps/spensive/doc/TODO
==============================================================================
--- (empty file)
+++ apps/spensive/doc/TODO	Tue Nov 20 12:10:05 2007
@@ -0,0 +1,37 @@
+V1
+
+
+* record expenses
+
+* stick expenses in reports
+
+* print reports
+
+* reports should have totals
+
+* attach images to expenses
+
+
+V2 
+
+
+* submit attachments for new or existing expenses
+* (create expenses by mail)
+
+
+v3
+
+    * organizations
+    * organizations have an expense admin
+    * organizations have staff
+    * for each staff person, organization admins can designate an approver
+
+    * staff can "submit" an expense report.
+    * the optional "approver" for the expense approves it
+    * the expense admin can mark it as "paid" or reject it
+    * rejected reports bounce back to the sender for edit or resend
+
+VLATER
+
+
+* report expenses by im

Modified: apps/spensive/etc/config.yml
==============================================================================
--- apps/spensive/etc/config.yml	(original)
+++ apps/spensive/etc/config.yml	Tue Nov 20 12:10:05 2007
@@ -19,8 +19,8 @@
     PoDir: share/po
   LogLevel: DEBUG
   Mailer: IO
-  MailerArgs: []
-
+  MailerArgs:
+    - %log/mail.log%
   Plugins: 
         - SkeletonApp: {}
         - ErrorTemplates: {}

Added: apps/spensive/lib/Spensive/Action/EmailDispatch.pm
==============================================================================
--- (empty file)
+++ apps/spensive/lib/Spensive/Action/EmailDispatch.pm	Tue Nov 20 12:10:05 2007
@@ -0,0 +1,171 @@
+use warnings;
+use strict;
+
+=head1 NAME
+
+Spensive::Action::EmailDispatch
+
+=cut
+
+package Spensive::Action::EmailDispatch;
+use base qw/Jifty::Action/;
+
+use Email::MIME;
+
+use Spensive::Notification::EmailError;
+
+=head2 arguments
+
+address
+
+email
+
+envelope_sender '--sender' => 'gooduser at example.com'
+
+=cut
+
+sub arguments {
+    {   address => {},
+        email   => {},
+        envelope_sender => {},
+    };
+}
+
+=head2 validate_address
+
+Make sure the email address looks sane
+
+=cut
+
+sub validate_address {
+    my $self  = shift;
+    my $email = shift;
+
+
+
+    my $pa = Spensive::Model::User->new(current_user => Jifty->app_class('CurrentUser')->superuser);
+    $pa->load_by_cols( published_address => $email );
+    if (    $pa->id ) {
+        return $self->validation_ok('address');
+    } else {
+        return $self->validation_error( address => "Address '$email' didn't match a published address" );
+    }
+
+}
+
+=head2 take_action
+
+Dispatches the C<email> based on the C<address> by looking up the
+appropriate L<Spensive::Model::User>.
+
+=cut
+
+sub take_action {
+    my $self = shift;
+
+    my $email = Email::MIME->new( $self->argument_value('email') );
+
+    my $body = $self->extract_body($email);
+    
+    # Load the sending address, making it a user if need be
+    my ($from) = Email::Address->parse( $email->header("From") );
+    return unless $from;
+    my $sender = Jifty->app_class('Model', 'User')->new(
+        current_user => Jifty->app_class('CurrentUser')->superuser );
+    $sender->load_or_create( email => $from->address );
+
+    # Load that as a CurrentUser
+    my $current = Jifty->app_class('CurrentUser')->new( id => $sender->id );
+    Jifty->web->temporary_current_user($current);
+
+    # See if this is a published address
+    my $address = $self->argument_value('address');
+    my $pa      = Jifty->app_class('Model', 'User')->new( current_user => Jifty->app_class('CurrentUser')->superuser );
+    $pa->load_by_cols( published_address => $address );
+
+    my $action;
+    if ( $pa->id ) { 
+        # We got a user
+            # if a user set up auto_attributes we need to parse
+            # for braindump syntax.  Put them first so that if
+            # auto_attributes contains [due] a [due] in Subject will clobber
+            my $summary = $email->header("Subject");
+
+            $action = Jifty->web->new_action(
+                class     => 'CreateExpense',
+                moniker   => "email_dispatch",
+                arguments => {
+                    title       => $summary,
+                    description   => $body,
+                    incurred_by => $pa->id,
+                    #email_content => $self->argument_value('email'),
+                },
+                # We clobber the current_user for group tasks so
+                # anyone can create group tasks, which is not usually
+                # the case
+                current_user => $pa
+            );
+        }
+    if($action) {
+        $action->run;
+        if($action->result->failure) {
+            $self->send_bounce( 'Spensive::Notification::EmailError', $action );
+            Jifty->log->warn("$action failed");
+        } else {
+            $self->result->message("Dispatched to an action $action");
+        }
+
+    } 
+    Jifty->web->temporary_current_user(undef);
+}
+
+=head2 send_bounce
+
+=cut
+
+sub send_bounce {
+    my $self   = shift;
+    my $class  = shift;
+    my $action = shift;
+
+    # Generate a bounce with the error message
+    my $to = Jifty->app_class('Model', 'User')->new(current_user => Jifty->app_class('CurrentUser')->superuser);
+    $to->load_or_create(email => $self->argument_value('envelope_sender'));
+    $class->new(
+        to      => $to,
+        address => $self->argument_value('address'),
+        result  => $action->result,
+        email   => $self->argument_value('email'),
+       )->send;
+}
+
+=head2 extract_body EMAIL
+
+Class method to extract the textual body from an Email::MIME email,
+and to attempt to guess the encoding appropriately
+
+=cut
+
+sub extract_body {
+    my $self = shift;
+    my $email = shift;
+
+    my $body = ( grep { ($_->content_type || '') =~ '^text/plain' } $email->parts )[0]
+      || $email;
+
+    my $content_type = $body->content_type
+      ? parse_content_type( $body->content_type )->{charset}
+      : '';
+    $body = $body->body;
+
+    Encode::_utf8_off($body);
+    
+    $body = Jifty::I18N->promote_encoding($body, $content_type);
+    
+    $body =~ s/\s*\n-- \n.*$//s;
+    
+    return $body;
+}
+
+
+1;

Modified: apps/spensive/lib/Spensive/Model/Attachment.pm
==============================================================================
--- apps/spensive/lib/Spensive/Model/Attachment.pm	(original)
+++ apps/spensive/lib/Spensive/Model/Attachment.pm	Tue Nov 20 12:10:05 2007
@@ -5,6 +5,9 @@
 use Jifty::DBI::Schema;
 
 use Spensive::Record schema {
+    column expense => references Spensive::Model::Expense;
+    column image => is ImageColumn;
+    column caption => type is 'text';
 
 };
 

Modified: apps/spensive/lib/Spensive/Model/Expense.pm
==============================================================================
--- apps/spensive/lib/Spensive/Model/Expense.pm	(original)
+++ apps/spensive/lib/Spensive/Model/Expense.pm	Tue Nov 20 12:10:05 2007
@@ -13,8 +13,8 @@
     column tags => type is 'text';
     column local_cost => type is 'float';
     column cost => type is 'float';
-    column payment_currency => type is 'text', valid_values are qw(USD CAD UKP EUR TWD NZD HKD);
-    column payment_method => type is 'text', valid_values are qw(personal_credit corporate_credit cash);
+    column payment_currency => type is 'text', valid_values are qw(USD CAD UKP EUR TWD NZD HKD), default is 'USD';
+    #column payment_method => type is 'text', valid_values are qw(personal_credit corporate_credit cash);
     column expense_report=> references Spensive::Model::ExpenseReport;
 };
 

Modified: apps/spensive/lib/Spensive/Model/User.pm
==============================================================================
--- apps/spensive/lib/Spensive/Model/User.pm	(original)
+++ apps/spensive/lib/Spensive/Model/User.pm	Tue Nov 20 12:10:05 2007
@@ -9,6 +9,7 @@
 
     column url => type is 'text';
     column accepted_eula => type is 'boolean';
+    column published_address => type is 'text';
 
 };
 

Added: apps/spensive/lib/Spensive/Notification/EmailError.pm
==============================================================================
--- (empty file)
+++ apps/spensive/lib/Spensive/Notification/EmailError.pm	Tue Nov 20 12:10:05 2007
@@ -0,0 +1,97 @@
+use warnings;
+use strict;
+
+=head1 NAME
+
+Spensive::Notification::EmailError -- A bounce message when there's an
+                                  error processing incoming mail
+
+=head1 DESCRIPTION
+
+We send an EmailError notification when we receive incoming mail that
+we get an error handling, for whatever reason.
+
+=cut
+
+package Spensive::Notification::EmailError;
+
+use base qw(Spensive::Notification);
+
+__PACKAGE__->mk_accessors(qw(result email address));
+
+=head2 result
+
+A Jifty::Result of the action that failed that caused us to send this
+bounce.
+
+=head2 email
+
+The text of the email that we received that generated the error.
+
+=head2 address
+
+The address to which the user attempted to send mail.
+
+=head2 setup
+
+Set up our subject and sender
+
+=cut
+
+sub setup {
+    my $self = shift;
+
+    $self->subject('Hiveminder.com -- Error processing email');
+    $self->from('Hiveminder <jesse at bestpractical.com>');
+}
+
+=head2 preface
+
+Return an apologetic message explaining that an error happened,
+including the text of the error.
+
+=cut
+
+sub preface {
+    my $self = shift;
+    my $to = $self->address;
+    my $error = $self->result->error;
+
+    return <<"END_PREFACE";
+We're sorry, but we encountered an error processing your email to us
+at $to.
+
+The error was: $error
+
+We hope we didn't mess up your day too badly with this. Drop us a line
+at support\@hiveminder.com if you need help fixing this problem, or
+try again in a little while.
+
+END_PREFACE
+
+}
+
+=head2 parts
+
+Returns the body, as well as an attachment of the original message.
+
+=cut
+
+sub parts {
+    my $self = shift;
+    return [
+        @{$self->SUPER::parts},
+        Email::MIME->create(
+            attributes => {
+                content_type => 'text/plain',
+                disposition => 'attachment',
+            },
+            body => Encode::encode_utf8($self->email),
+           )
+       ];
+}
+
+
+
+
+1;

Modified: apps/spensive/lib/Spensive/View.pm
==============================================================================
--- apps/spensive/lib/Spensive/View.pm	(original)
+++ apps/spensive/lib/Spensive/View.pm	Tue Nov 20 12:10:05 2007
@@ -6,15 +6,15 @@
 
 template 'index.html' => page {
 
-    h1 { _('Spensive helps you keep track of your expenses') };
+    h1 { _('Spensive helps you keep track of your business expenses') };
     h2 { _('(It also helps you get reimbursed for them)') };
 
+    h3 { _('Unsubmitted expenses') };
+    span { {class is 'hint'}; hyperlink(label => 'All my expenses...', path => '/expenses/all') };
     form {
         my $search = Spensive::Model::ExpenseCollection->new();
-        $search->limit(
-            column => 'incurred_by',
-            value  => Jifty->web->current_user->id
-        );
+        $search->limit( column => 'expense_report', value => 'null', operator => 'is',  quote_value => 0);
+        $search->limit( column => 'incurred_by', value  => Jifty->web->current_user->id);
         set (search_collection =>  $search);
         render_region(
             name => 'expenses',
@@ -25,6 +25,8 @@
         );
     };
 
+    h3 { _('Open expense reports')};
+    span { {class is 'hint'}; hyperlink(label => 'All my expense reports...', path => '/reports/all') };
     form {
         my $search = Spensive::Model::ExpenseReportCollection->new();
         $search->limit(
@@ -61,8 +63,19 @@
 
 template report_tabs => sub {
     my $self = shift;
-    $self->render_tabs( 'reports', [qw(report)],
-        qw(attach_reports_tab remove_reports_tab ) );
+    $self->render_tabs(
+        'reports',
+        [qw(report)],
+        { label => 'Add expenses', path => '/attach_reports' ,
+        defer => 1
+    },
+        {   label => 'Remove expenses',
+            path  => '/remove_reports',
+            defer => 1,
+        }
+    );
+
+    #        qw(/attach_reports_tab /remove_reports_tab ) );
 };
 
 template 'attach_reports' => sub {


More information about the Jifty-commit mailing list