From 15d3b8f05165917ea52e52e3b2a8d2da3d4e21c0 Mon Sep 17 00:00:00 2001 From: Jonathan Labreuille Date: Fri, 2 Oct 2015 13:01:27 +0200 Subject: [PATCH] Send newsletter + Listing + Last Step - fixed Selection React - fixed bulk actions (side effect of muti selection) - added actual sending of newsletter - added Setting::getValue($key, $default) in order to get settings - improved Bridge class to allow override of from/reply_to - added jquery.serializeObject to ease the pain when posting form data --- assets/js/src/form/fields/selection.jsx | 46 ++-------- assets/js/src/jquery.serialize_object.js | 107 +++++++++++++++++++++++ assets/js/src/newsletters/send.jsx | 41 +++++++-- assets/js/src/subscribers/list.jsx | 6 +- lib/Mailer/Bridge.php | 60 +++++++++---- lib/Models/Setting.php | 19 ++-- lib/Router/Newsletters.php | 45 ++++++++-- webpack.config.js | 8 +- 8 files changed, 257 insertions(+), 75 deletions(-) create mode 100644 assets/js/src/jquery.serialize_object.js diff --git a/assets/js/src/form/fields/selection.jsx b/assets/js/src/form/fields/selection.jsx index 4a6560a12b..d91fa7c776 100644 --- a/assets/js/src/form/fields/selection.jsx +++ b/assets/js/src/form/fields/selection.jsx @@ -15,12 +15,14 @@ function( } }, componentWillMount: function() { - this.loadCachedItems(); + this.loadCachedItems(); }, componentDidMount: function() { - jQuery('#'+this.props.id).select2({ - width: '25em' - }); + if(this.props.select2) { + jQuery('#'+this.props.id).select2({ + width: '25em' + }); + } }, loadCachedItems: function() { if(typeof(window['mailpoet_'+this.props.endpoint]) !== 'undefined') { @@ -30,36 +32,6 @@ function( }); } }, - loadItems: function() { - this.setState({ loading: true }); - - MailPoet.Ajax.post({ - endpoint: this.props.endpoint, - action: 'listing', - data: { - 'offset': 0, - 'limit': 100, - 'search': '', - 'sort_by': 'name', - 'sort_order': 'asc' - } - }) - .done(function(response) { - if(this.isMounted()) { - if(response === false) { - this.setState({ - loading: false, - items: [] - }); - } else { - this.setState({ - loading: false, - items: response.items - }); - } - } - }.bind(this)); - }, handleChange: function() { this.setState({ selected: jQuery('#'+this.props.id).val() @@ -82,11 +54,9 @@ function( return ( diff --git a/assets/js/src/jquery.serialize_object.js b/assets/js/src/jquery.serialize_object.js new file mode 100644 index 0000000000..6db1ba9f23 --- /dev/null +++ b/assets/js/src/jquery.serialize_object.js @@ -0,0 +1,107 @@ +define( + [ + 'jquery' + ], + function( + $ + ) { + // Combination of jQuery.deparam and jQuery.serializeObject by Ben Alman. + /*! + * jQuery BBQ: Back Button & Query Library - v1.2.1 - 2/17/2010 + * http://benalman.com/projects/jquery-bbq-plugin/ + * + * Copyright (c) 2010 "Cowboy" Ben Alman + * Dual licensed under the MIT and GPL licenses. + * http://benalman.com/about/license/ + */ + /*! + * jQuery serializeObject - v0.2 - 1/20/2010 + * http://benalman.com/projects/jquery-misc-plugins/ + * + * Copyright (c) 2010 "Cowboy" Ben Alman + * Dual licensed under the MIT and GPL licenses. + * http://benalman.com/about/license/ + */ + $.fn.serializeObject = function(coerce) { + var obj = {}, + coerce_types = { 'true': !0, 'false': !1, 'null': null }; + + // Iterate over all name=value pairs. + $.each( this.serializeArray(), function(j,v){ + var key = v.name, + val = v.value, + cur = obj, + i = 0, + + // If key is more complex than 'foo', like 'a[]' or 'a[b][c]', split it + // into its component parts. + keys = key.split( '][' ), + keys_last = keys.length - 1; + + // If the first keys part contains [ and the last ends with ], then [] + // are correctly balanced. + if ( /\[/.test( keys[0] ) && /\]$/.test( keys[ keys_last ] ) ) { + // Remove the trailing ] from the last keys part. + keys[ keys_last ] = keys[ keys_last ].replace( /\]$/, '' ); + + // Split first keys part into two parts on the [ and add them back onto + // the beginning of the keys array. + keys = keys.shift().split('[').concat( keys ); + + keys_last = keys.length - 1; + } else { + // Basic 'foo' style key. + keys_last = 0; + } + + // Coerce values. + if ( coerce ) { + val = val && !isNaN(val) ? +val // number + : val === 'undefined' ? undefined // undefined + : coerce_types[val] !== undefined ? coerce_types[val] // true, false, null + : val; // string + } + + if ( keys_last ) { + // Complex key, build deep object structure based on a few rules: + // * The 'cur' pointer starts at the object top-level. + // * [] = array push (n is set to array length), [n] = array if n is + // numeric, otherwise object. + // * If at the last keys part, set the value. + // * For each keys part, if the current level is undefined create an + // object or array based on the type of the next keys part. + // * Move the 'cur' pointer to the next level. + // * Rinse & repeat. + for ( ; i <= keys_last; i++ ) { + key = keys[i] === '' ? cur.length : keys[i]; + cur = cur[key] = i < keys_last + ? cur[key] || ( keys[i+1] && isNaN( keys[i+1] ) ? {} : [] ) + : val; + } + + } else { + // Simple key, even simpler rules, since only scalars and shallow + // arrays are allowed. + + if ( $.isArray( obj[key] ) ) { + // val is already an array, so push on the next value. + obj[key].push( val ); + + } else if ( obj[key] !== undefined ) { + // val isn't an array, but since a second value has been specified, + // convert val into an array. + obj[key] = [ obj[key], val ]; + + } else { + // val is a scalar. + obj[key] = val; + } + } + }); + + return obj; + }; + + return $; + } +); \ No newline at end of file diff --git a/assets/js/src/newsletters/send.jsx b/assets/js/src/newsletters/send.jsx index 15054b6e9b..fb8cb82022 100644 --- a/assets/js/src/newsletters/send.jsx +++ b/assets/js/src/newsletters/send.jsx @@ -1,6 +1,7 @@ define( [ 'react', + 'react-router', 'mailpoet', 'form/form.jsx', 'form/fields/selection.jsx', @@ -8,6 +9,7 @@ define( ], function( React, + Router, MailPoet, Form, Selection, @@ -32,7 +34,9 @@ define( + endpoint="segments" + multiple={ true } + select2={ true } /> ) }, { @@ -77,15 +81,42 @@ define( var messages = { updated: function() { - MailPoet.Notice.success('Newsletter succesfully updated!'); + MailPoet.Notice.success('The newsletter has been updated!'); } }; var NewsletterSend = React.createClass({ + mixins: [ + Router.Navigation + ], handleSend: function() { - console.log('send.'); - console.log(jQuery('#mailpoet_newsletter').serializeArray()); - console.log(jQuery('#mailpoet_segments').val()); + MailPoet.Ajax.post({ + endpoint: 'newsletters', + action: 'send', + data: { + id: this.props.params.id, + newsletter: jQuery('#mailpoet_newsletter').serializeObject(), + segments: jQuery('#mailpoet_segments').val() + } + }).done(function(response) { + if(response === true) { + this.transitionTo('/'); + MailPoet.Notice.success( + 'The newsletter has been sent!' + ); + } else { + if(response.errors) { + MailPoet.Notice.error( + response.errors.join("
") + ); + } else { + MailPoet.Notice.error( + 'An error occurred while trying to send. '+ + 'Check your settings.' + ); + } + } + }.bind(this)); }, render: function() { return ( diff --git a/assets/js/src/subscribers/list.jsx b/assets/js/src/subscribers/list.jsx index 3a10cbf993..f63b6dadf7 100644 --- a/assets/js/src/subscribers/list.jsx +++ b/assets/js/src/subscribers/list.jsx @@ -65,7 +65,7 @@ define( label: 'Move to list...', onSelect: function() { return ( - ); @@ -81,7 +81,7 @@ define( label: 'Add to list...', onSelect: function() { return ( - ); @@ -97,7 +97,7 @@ define( label: 'Remove from list...', onSelect: function() { return ( - ); diff --git a/lib/Mailer/Bridge.php b/lib/Mailer/Bridge.php index bb429e55c7..c86f602f4a 100644 --- a/lib/Mailer/Bridge.php +++ b/lib/Mailer/Bridge.php @@ -6,21 +6,43 @@ use MailPoet\Models\Setting; if(!defined('ABSPATH')) exit; class Bridge { + protected $from_address = null; + protected $from_name = ''; + protected $reply_to_address = null; + protected $reply_to_name = ''; + protected $newsletter = null; + protected $subscribers = null; + protected $api_key = null; + function __construct($newsletter, $subscribers) { $this->newsletter = $newsletter; $this->subscribers = $subscribers; - $this->from_name = - Setting::where('name', 'from_name') - ->findOne()->value; + $this->from_address = ( + isset($this->newsletter['from_address']) + ) + ? $this->newsletter['from_address'] + : Setting::getValue('from_address'); - $this->from_address = - Setting::where('name', 'from_address') - ->findOne()->value; + $this->from_name = ( + isset($this->newsletter['from_name']) + ) + ? $this->newsletter['from_name'] + : Setting::getValue('from_name', ''); - $this->api_key = - Setting::where('name', 'api_key') - ->findOne()->value; + $this->reply_to_address = ( + isset($this->newsletter['reply_to_address']) + ) + ? $this->newsletter['reply_to_address'] + : Setting::getValue('reply_to_address'); + + $this->reply_to_name = ( + isset($this->newsletter['reply_to_name']) + ) + ? $this->newsletter['reply_to_name'] + : Setting::getValue('reply_to_name', ''); + + $this->api_key = Setting::where('name', 'api_key')->findOne()->value; } function messages() { @@ -32,19 +54,27 @@ class Bridge { } function generateMessage($subscriber) { - return array( + $message = array( 'subject' => $this->newsletter['subject'], - 'to' => (array( + 'to' => array( 'address' => $subscriber['email'], 'name' => $subscriber['first_name'].' '.$subscriber['last_name'] - )), - 'from' => (array( + ), + 'from' => array( 'address' => $this->from_address, 'name' => $this->from_name - )), + ), 'text' => "", 'html' => $this->newsletter['body'] ); + + if($this->reply_to_address !== null) { + $message['reply_to'] = array( + 'address' => $this->reply_to_address, + 'name' => $this->reply_to_name + ); + } + return $message; } function auth() { @@ -74,7 +104,7 @@ class Bridge { ); $success = - (wp_remote_retrieve_response_code($result)===201); + (wp_remote_retrieve_response_code($result) === 201); return $success; } diff --git a/lib/Models/Setting.php b/lib/Models/Setting.php index 18e108c40e..d50c75b31c 100644 --- a/lib/Models/Setting.php +++ b/lib/Models/Setting.php @@ -9,16 +9,21 @@ class Setting extends Model { function __construct() { parent::__construct(); - $this->addValidations("name", array( - "required" => "name_is_blank", - "isString" => "name_is_not_string" - )); - $this->addValidations("value", array( - "required" => "value_is_blank", - "isString" => "value_is_not_string" + $this->addValidations('name', array( + 'required' => 'name_is_blank', + 'isString' => 'name_is_not_string' )); } + public static function getValue($key, $default = null) { + $setting = Setting::where('name', $key)->findOne(); + if($setting === false) { + return $default; + } else { + return $setting->value; + } + } + public static function createOrUpdate($model) { $exists = self::where('name', $model['name']) ->find_one(); diff --git a/lib/Router/Newsletters.php b/lib/Router/Newsletters.php index 63daf3436a..1a3ab07762 100644 --- a/lib/Router/Newsletters.php +++ b/lib/Router/Newsletters.php @@ -4,6 +4,7 @@ namespace MailPoet\Router; use MailPoet\Listing; use MailPoet\Mailer\Bridge; use MailPoet\Models\Newsletter; +use MailPoet\Models\Segment; use MailPoet\Models\Subscriber; use MailPoet\Models\NewsletterTemplate; use MailPoet\Newsletter\Renderer\Renderer; @@ -25,7 +26,7 @@ class Newsletters { } function getAll() { - $collection = Newsletter::find_array(); + $collection = Newsletter::findArray(); wp_send_json($collection); } @@ -48,11 +49,43 @@ class Newsletters { wp_send_json($result); } - function send($id) { - $newsletter = Newsletter::find_one($id) - ->as_array(); - $subscribers = Subscriber::find_array(); - $mailer = new Bridge($newsletter, $subscribers); + function send($data = array()) { + $newsletter = Newsletter::findOne($data['id'])->asArray(); + + if(empty($data['segments'])) { + return wp_send_json(array( + 'errors' => array( + __("You need to select a list.") + ) + )); + } + + $segments = Segment::whereIdIn($data['segments'])->findMany(); + $subscribers = array(); + foreach($segments as $segment) { + $segment_subscribers = $segment->subscribers()->findMany(); + foreach($segment_subscribers as $segment_subscriber) { + $subscribers[$segment_subscriber->email] = $segment_subscriber + ->asArray(); + } + } + + if(empty($subscribers)) { + return wp_send_json(array( + 'errors' => array( + __("No subscribers found.") + ) + )); + } + + // TO REMOVE once we add the columns from/reply_to + $newsletter = array_merge($newsletter, $data['newsletter']); + // END - TO REMOVE + + $renderer = new Renderer(json_decode($newsletter['body'], true)); + $newsletter['body'] = $renderer->renderAll(); + + $mailer = new Bridge($newsletter, array_values($subscribers)); wp_send_json($mailer->send()); } diff --git a/webpack.config.js b/webpack.config.js index 13290009a9..b90589ea77 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -58,7 +58,13 @@ config.push(_.extend({}, baseConfig, { name: 'admin', entry: { vendor: ['handlebars', 'handlebars_helpers'], - mailpoet: ['mailpoet', 'ajax', 'modal', 'notice'], + mailpoet: [ + 'mailpoet', + 'ajax', + 'modal', + 'notice', + 'jquery.serialize_object' + ], admin: [ 'settings.jsx', 'subscribers/subscribers.jsx',