Conditional display of statistics column (for standard)

- improved duplicate action (for standard)
- moved STATUS_COMPLETED constant from worker to SendingQueue model where it belongs
This commit is contained in:
Jonathan Labreuille
2016-06-14 13:18:27 +02:00
parent cabfd8a946
commit ee6e261c42
10 changed files with 209 additions and 109 deletions

View File

@ -676,6 +676,12 @@ define(
sort_by = this.state.sort_by, sort_by = this.state.sort_by,
sort_order = this.state.sort_order; sort_order = this.state.sort_order;
// columns
var columns = this.props.columns || [];
columns = columns.filter(function(column) {
return (column.display === undefined || !!(column.display) === true);
});
// bulk actions // bulk actions
var bulk_actions = this.props.bulk_actions || []; var bulk_actions = this.props.bulk_actions || [];
@ -761,7 +767,7 @@ define(
selection={ this.state.selection } selection={ this.state.selection }
sort_by={ sort_by } sort_by={ sort_by }
sort_order={ sort_order } sort_order={ sort_order }
columns={ this.props.columns } columns={ columns }
is_selectable={ bulk_actions.length > 0 } /> is_selectable={ bulk_actions.length > 0 } />
</thead> </thead>
@ -771,7 +777,7 @@ define(
onRestoreItem={ this.handleRestoreItem } onRestoreItem={ this.handleRestoreItem }
onTrashItem={ this.handleTrashItem } onTrashItem={ this.handleTrashItem }
onRefreshItems={ this.handleRefreshItems } onRefreshItems={ this.handleRefreshItems }
columns={ this.props.columns } columns={ columns }
is_selectable={ bulk_actions.length > 0 } is_selectable={ bulk_actions.length > 0 }
onSelectItem={ this.handleSelectItem } onSelectItem={ this.handleSelectItem }
onSelectAll={ this.handleSelectAll } onSelectAll={ this.handleSelectAll }
@ -791,7 +797,7 @@ define(
selection={ this.state.selection } selection={ this.state.selection }
sort_by={ sort_by } sort_by={ sort_by }
sort_order={ sort_order } sort_order={ sort_order }
columns={ this.props.columns } columns={ columns }
is_selectable={ bulk_actions.length > 0 } /> is_selectable={ bulk_actions.length > 0 } />
</tfoot> </tfoot>

View File

@ -68,17 +68,12 @@ var columns = [
label: MailPoet.I18n.t('status') label: MailPoet.I18n.t('status')
}, },
{ {
name: 'segments', name: 'settings',
label: MailPoet.I18n.t('lists') label: MailPoet.I18n.t('settings')
}, },
{ {
name: 'statistics', name: 'history',
label: MailPoet.I18n.t('statistics') label: MailPoet.I18n.t('history')
},
{
name: 'created_at',
label: MailPoet.I18n.t('createdOn'),
sortable: true
}, },
{ {
name: 'updated_at', name: 'updated_at',
@ -96,6 +91,16 @@ var bulk_actions = [
]; ];
var newsletter_actions = [ var newsletter_actions = [
{
name: 'view',
link: function(newsletter) {
return (
<a href={ newsletter.preview_url } target="_blank">
{MailPoet.I18n.t('preview')}
</a>
);
}
},
{ {
name: 'edit', name: 'edit',
link: function(newsletter) { link: function(newsletter) {
@ -106,6 +111,26 @@ var newsletter_actions = [
); );
} }
}, },
{
name: 'duplicate',
label: MailPoet.I18n.t('duplicate'),
onClick: function(newsletter, refresh) {
return MailPoet.Ajax.post({
endpoint: 'newsletters',
action: 'duplicate',
data: newsletter.id
}).done(function(response) {
if (response !== false && response.subject !== undefined) {
MailPoet.Notice.success(
(MailPoet.I18n.t('newsletterDuplicated')).replace(
'%$1s', response.subject
)
);
}
refresh();
});
}
},
{ {
name: 'trash' name: 'trash'
} }
@ -158,31 +183,6 @@ const NewsletterListNotification = React.createClass({
</span> </span>
); );
}, },
renderStatistics: function(newsletter) {
if (!newsletter.statistics || !newsletter.queue || newsletter.queue.count_processed == 0 || newsletter.queue.status === 'scheduled') {
return (
<span>
{MailPoet.I18n.t('notSentYet')}
</span>
);
}
var percentage_clicked = Math.round(
(newsletter.statistics.clicked * 100) / (newsletter.queue.count_processed)
);
var percentage_opened = Math.round(
(newsletter.statistics.opened * 100) / (newsletter.queue.count_processed)
);
var percentage_unsubscribed = Math.round(
(newsletter.statistics.unsubscribed * 100) / (newsletter.queue.count_processed)
);
return (
<span>
{ percentage_opened }%, { percentage_clicked }%, { percentage_unsubscribed }%
</span>
);
},
renderItem: function(newsletter, actions) { renderItem: function(newsletter, actions) {
var rowClasses = classNames( var rowClasses = classNames(
'manage-column', 'manage-column',
@ -190,10 +190,6 @@ const NewsletterListNotification = React.createClass({
'has-row-actions' 'has-row-actions'
); );
var segments = newsletter.segments.map(function(segment) {
return segment.name
}).join(', ');
return ( return (
<div> <div>
<td className={ rowClasses }> <td className={ rowClasses }>
@ -207,14 +203,11 @@ const NewsletterListNotification = React.createClass({
<td className="column" data-colname={ MailPoet.I18n.t('status') }> <td className="column" data-colname={ MailPoet.I18n.t('status') }>
{ this.renderStatus(newsletter) } { this.renderStatus(newsletter) }
</td> </td>
<td className="column" data-colname={ MailPoet.I18n.t('lists') }> <td className="column" data-colname={ MailPoet.I18n.t('settings') }>
{ segments } { this.renderSettings(newsletter) }
</td> </td>
<td className="column" data-colname={ MailPoet.I18n.t('statistics') }> <td className="column" data-colname={ MailPoet.I18n.t('history') }>
{ this.renderStatistics(newsletter) } <a href="#TODO">{ MailPoet.I18n.t('viewHistory') }</a>
</td>
<td className="column-date" data-colname={ MailPoet.I18n.t('createdOn') }>
<abbr>{ MailPoet.Date.format(newsletter.created_at) }</abbr>
</td> </td>
<td className="column-date" data-colname={ MailPoet.I18n.t('lastModifiedOn') }> <td className="column-date" data-colname={ MailPoet.I18n.t('lastModifiedOn') }>
<abbr>{ MailPoet.Date.format(newsletter.updated_at) }</abbr> <abbr>{ MailPoet.Date.format(newsletter.updated_at) }</abbr>

View File

@ -9,6 +9,8 @@ import classNames from 'classnames'
import jQuery from 'jquery' import jQuery from 'jquery'
import MailPoet from 'mailpoet' import MailPoet from 'mailpoet'
const mailpoet_tracking_enabled = (!!(window['mailpoet_tracking_enabled']));
const messages = { const messages = {
onTrash(response) { onTrash(response) {
const count = ~~response; const count = ~~response;
@ -73,7 +75,8 @@ var columns = [
}, },
{ {
name: 'statistics', name: 'statistics',
label: MailPoet.I18n.t('statistics') label: MailPoet.I18n.t('statistics'),
display: mailpoet_tracking_enabled
}, },
{ {
name: 'updated_at', name: 'updated_at',
@ -82,6 +85,7 @@ var columns = [
} }
]; ];
var bulk_actions = [ var bulk_actions = [
{ {
name: 'trash', name: 'trash',
@ -231,30 +235,33 @@ const NewsletterListStandard = React.createClass({
); );
} }
}, },
renderStatistics: function(item) { renderStatistics: function(newsletter) {
if(!item.statistics || !item.queue || item.queue.count_processed == 0 || item.queue.status === 'scheduled') { if (mailpoet_tracking_enabled === false) {
return ( return;
<span>
{MailPoet.I18n.t('notSentYet')}
</span>
);
} }
var percentage_clicked = Math.round( if(newsletter.statistics && newsletter.queue && newsletter.queue.status !== 'scheduled') {
(item.statistics.clicked * 100) / (item.queue.count_processed) const total_sent = ~~(newsletter.queue.count_processed);
); const percentage_clicked = Math.round(
var percentage_opened = Math.round( (~~(newsletter.statistics.clicked) * 100) / total_sent
(item.statistics.opened * 100) / (item.queue.count_processed) );
); const percentage_opened = Math.round(
var percentage_unsubscribed = Math.round( (~~(newsletter.statistics.opened) * 100) / total_sent
(item.statistics.unsubscribed * 100) / (item.queue.count_processed) );
); const percentage_unsubscribed = Math.round(
(~~(newsletter.statistics.unsubscribed) * 100) / total_sent
);
return ( return (
<span> <span>
{ percentage_opened }%, { percentage_clicked }%, { percentage_unsubscribed }% { percentage_opened }%, { percentage_clicked }%, { percentage_unsubscribed }%
</span> </span>
); );
} else {
return (
<span>{MailPoet.I18n.t('notSentYet')}</span>
);
}
}, },
renderItem: function(newsletter, actions) { renderItem: function(newsletter, actions) {
var rowClasses = classNames( var rowClasses = classNames(
@ -283,9 +290,11 @@ const NewsletterListStandard = React.createClass({
<td className="column" data-colname={ MailPoet.I18n.t('lists') }> <td className="column" data-colname={ MailPoet.I18n.t('lists') }>
{ segments } { segments }
</td> </td>
<td className="column" data-colname={ MailPoet.I18n.t('statistics') }> {(mailpoet_tracking_enabled === true) ? (
{ this.renderStatistics(newsletter) } <td className="column" data-colname={ MailPoet.I18n.t('statistics') }>
</td> { this.renderStatistics(newsletter) }
</td>
) : null }
<td className="column-date" data-colname={ MailPoet.I18n.t('lastModifiedOn') }> <td className="column-date" data-colname={ MailPoet.I18n.t('lastModifiedOn') }>
<abbr>{ MailPoet.Date.format(newsletter.updated_at) }</abbr> <abbr>{ MailPoet.Date.format(newsletter.updated_at) }</abbr>
</td> </td>

View File

@ -8,6 +8,7 @@ import ListingTabs from 'newsletters/listings/tabs.jsx'
import classNames from 'classnames' import classNames from 'classnames'
import jQuery from 'jquery' import jQuery from 'jquery'
import MailPoet from 'mailpoet' import MailPoet from 'mailpoet'
import _ from 'underscore'
const messages = { const messages = {
onTrash(response) { onTrash(response) {
@ -165,21 +166,48 @@ const NewsletterListWelcome = React.createClass({
}.bind(this)); }.bind(this));
}, },
renderStatus: function(newsletter) { renderStatus: function(newsletter) {
let total_sent;
total_sent = (
MailPoet.I18n.t('sentToXSubscribers')
.replace('%$1d', newsletter.total_sent)
);
return ( return (
<select <div>
data-id={ newsletter.id } <select
defaultValue={ newsletter.status } data-id={ newsletter.id }
onChange={ this.updateStatus } defaultValue={ newsletter.status }
> onChange={ this.updateStatus }
<option value="active">{ MailPoet.I18n.t('active') }</option> >
<option value="draft">{ MailPoet.I18n.t('inactive') }</option> <option value="active">{ MailPoet.I18n.t('active') }</option>
</select> <option value="draft">{ MailPoet.I18n.t('inactive') }</option>
</select>
<p>{ total_sent }</p>
</div>
); );
}, },
renderSettings: function(newsletter) { renderSettings: function(newsletter) {
let settings;
switch (newsletter.options.event) {
case 'user':
// WP User
settings = MailPoet.I18n.t('onWordpressUserRegistration');
break;
case 'segment':
// get segment
const segment = _.find(mailpoet_segments, function(segment) {
return (~~(segment.id) === ~~(newsletter.options.segment));
});
settings = MailPoet.I18n.t('onSubscriptionToList') + ' ' +segment.name;
break;
}
return ( return (
<span> <span>
Settings... { settings }
</span> </span>
); );
}, },

View File

@ -413,6 +413,8 @@ class Menu {
24 24
); );
$data['tracking_enabled'] = Setting::getValue('tracking.enabled');
wp_enqueue_script('jquery-ui'); wp_enqueue_script('jquery-ui');
wp_enqueue_script('jquery-ui-datepicker'); wp_enqueue_script('jquery-ui-datepicker');

View File

@ -4,6 +4,7 @@ namespace MailPoet\Cron\Workers;
use MailPoet\Cron\CronHelper; use MailPoet\Cron\CronHelper;
use MailPoet\Mailer\Mailer; use MailPoet\Mailer\Mailer;
use MailPoet\Models\Newsletter; use MailPoet\Models\Newsletter;
use MailPoet\Models\SendingQueue as SendingQueueModel;
use MailPoet\Models\NewsletterPost; use MailPoet\Models\NewsletterPost;
use MailPoet\Models\Setting; use MailPoet\Models\Setting;
use MailPoet\Models\StatisticsNewsletters; use MailPoet\Models\StatisticsNewsletters;
@ -23,7 +24,6 @@ class SendingQueue {
private $timer; private $timer;
const BATCH_SIZE = 50; const BATCH_SIZE = 50;
const DIVIDER = '***MailPoet***'; const DIVIDER = '***MailPoet***';
const STATUS_COMPLETED = 'completed';
function __construct($timer = false) { function __construct($timer = false) {
$this->mta_config = $this->getMailerConfig(); $this->mta_config = $this->getMailerConfig();
@ -295,7 +295,7 @@ class SendingQueue {
} }
function getQueues() { function getQueues() {
return \MailPoet\Models\SendingQueue::orderByDesc('priority') return SendingQueueModel::orderByDesc('priority')
->whereNull('deleted_at') ->whereNull('deleted_at')
->whereNull('status') ->whereNull('status')
->findResultSet(); ->findResultSet();
@ -321,7 +321,7 @@ class SendingQueue {
$queue->count_processed + $queue->count_to_process; $queue->count_processed + $queue->count_to_process;
if(!$queue->count_to_process) { if(!$queue->count_to_process) {
$queue->processed_at = current_time('mysql'); $queue->processed_at = current_time('mysql');
$queue->status = self::STATUS_COMPLETED; $queue->status = SendingQueueModel::STATUS_COMPLETED;
} }
$queue->subscribers = serialize((array) $queue->subscribers); $queue->subscribers = serialize((array) $queue->subscribers);
$queue->save(); $queue->save();

View File

@ -1,5 +1,6 @@
<?php <?php
namespace MailPoet\Models; namespace MailPoet\Models;
use MailPoet\Util\Helpers;
if(!defined('ABSPATH')) exit; if(!defined('ABSPATH')) exit;
@ -55,11 +56,19 @@ class Newsletter extends Model {
} }
function duplicate($data = array()) { function duplicate($data = array()) {
$data = $this->asArray(); // get current newsletter's data as an array
unset($data['id']); $newsletter_data = $this->asArray();
$duplicate = self::create(); // remove id so that it creates a new record
unset($newsletter_data['id']);
// merge data with newsletter data (allows override)
$data = array_merge($newsletter_data, $data);
$duplicate = self::create();
$duplicate->hydrate($data); $duplicate->hydrate($data);
// reset timestamps
$duplicate->set_expr('created_at', 'NOW()'); $duplicate->set_expr('created_at', 'NOW()');
$duplicate->set_expr('updated_at', 'NOW()'); $duplicate->set_expr('updated_at', 'NOW()');
$duplicate->set_expr('deleted_at', 'NULL'); $duplicate->set_expr('deleted_at', 'NULL');
@ -67,15 +76,25 @@ class Newsletter extends Model {
// reset status // reset status
$duplicate->set('status', self::STATUS_DRAFT); $duplicate->set('status', self::STATUS_DRAFT);
// TODO: duplicate segments linked (if need be) $duplicate->save();
// TODO: duplicate options (if need be) if($duplicate->getErrors() === false) {
// create relationships between duplicate and segments
$segments = $this->segments()->findArray();
if($duplicate->save()) { if(!empty($segments)) {
return $duplicate; foreach($segments as $segment) {
} else { $relation = NewsletterSegment::create();
return false; $relation->segment_id = $segment['id'];
$relation->newsletter_id = $duplicate->id();
$result = $relation->save();
}
}
// TODO: duplicate options (if need be)
} }
return $duplicate;
} }
function asArray() { function asArray() {
@ -133,6 +152,24 @@ class Newsletter extends Model {
return $this; return $this;
} }
function withOptions() {
$options = $this->options()->findArray();
if(empty($options)) {
$this->options = array();
} else {
$this->options = Helpers::arrayColumn($options, 'value', 'name');
}
return $this;
}
function withTotalSent() {
// total of subscribers who received the email
$this->total_sent = (int)SendingQueue::where('newsletter_id', $this->id)
->where('status', SendingQueue::STATUS_COMPLETED)
->sum('count_processed');
return $this;
}
function withStatistics() { function withStatistics() {
$statistics = $this->getStatistics(); $statistics = $this->getStatistics();
if($statistics === false) { if($statistics === false) {
@ -140,6 +177,7 @@ class Newsletter extends Model {
} else { } else {
$this->statistics = $statistics->asArray(); $this->statistics = $statistics->asArray();
} }
return $this; return $this;
} }
@ -149,9 +187,9 @@ class Newsletter extends Model {
} }
return SendingQueue::tableAlias('queues') return SendingQueue::tableAlias('queues')
->selectExpr( ->selectExpr(
'count(DISTINCT(clicks.subscriber_id)) as clicked, ' . 'COUNT(DISTINCT(clicks.subscriber_id)) as clicked, ' .
'count(DISTINCT(opens.subscriber_id)) as opened, ' . 'COUNT(DISTINCT(opens.subscriber_id)) as opened, ' .
'count(DISTINCT(unsubscribes.subscriber_id)) as unsubscribed ' 'COUNT(DISTINCT(unsubscribes.subscriber_id)) as unsubscribed '
) )
->leftOuterJoin( ->leftOuterJoin(
MP_STATISTICS_CLICKS_TABLE, MP_STATISTICS_CLICKS_TABLE,
@ -299,14 +337,6 @@ class Newsletter extends Model {
case self::TYPE_WELCOME: case self::TYPE_WELCOME:
case self::TYPE_NOTIFICATION: case self::TYPE_NOTIFICATION:
$groups = array_merge($groups, array( $groups = array_merge($groups, array(
array(
'name' => self::STATUS_DRAFT,
'label' => __('Not active'),
'count' => Newsletter::getPublished()
->filter('filterType', $type)
->filter('filterStatus', self::STATUS_DRAFT)
->count()
),
array( array(
'name' => self::STATUS_ACTIVE, 'name' => self::STATUS_ACTIVE,
'label' => __('Active'), 'label' => __('Active'),
@ -314,6 +344,14 @@ class Newsletter extends Model {
->filter('filterType', $type) ->filter('filterType', $type)
->filter('filterStatus', self::STATUS_ACTIVE) ->filter('filterStatus', self::STATUS_ACTIVE)
->count() ->count()
),
array(
'name' => self::STATUS_DRAFT,
'label' => __('Not active'),
'count' => Newsletter::getPublished()
->filter('filterType', $type)
->filter('filterStatus', self::STATUS_DRAFT)
->count()
) )
)); ));
break; break;
@ -379,7 +417,15 @@ class Newsletter extends Model {
} }
static function listingQuery($data = array()) { static function listingQuery($data = array()) {
return self::filter('filterType', $data['tab']) return self::select(array(
'id',
'subject',
'type',
'status',
'updated_at',
'deleted_at'
))
->filter('filterType', $data['tab'])
->filter('filterBy', $data) ->filter('filterBy', $data)
->filter('groupBy', $data) ->filter('groupBy', $data)
->filter('search', $data['search']); ->filter('search', $data['search']);

View File

@ -6,6 +6,8 @@ if(!defined('ABSPATH')) exit;
class SendingQueue extends Model { class SendingQueue extends Model {
public static $_table = MP_SENDING_QUEUES_TABLE; public static $_table = MP_SENDING_QUEUES_TABLE;
const STATUS_COMPLETED = 'completed';
function __construct() { function __construct() {
parent::__construct(); parent::__construct();
} }

View File

@ -150,9 +150,13 @@ class Newsletters {
$newsletter = Newsletter::findOne($id); $newsletter = Newsletter::findOne($id);
if($newsletter !== false) { if($newsletter !== false) {
$result = $newsletter->duplicate(array( $duplicate = $newsletter->duplicate(array(
'subject' => sprintf(__('Copy of %s'), $newsletter->subject) 'subject' => sprintf(__('Copy of %s'), $newsletter->subject)
))->asArray(); ));
if($duplicate !== false && $duplicate->getErrors() === false) {
$result = $newsletter->asArray();
}
} }
return $result; return $result;
} }
@ -256,7 +260,13 @@ class Newsletters {
->withStatistics(); ->withStatistics();
} else if($newsletter->type === Newsletter::TYPE_WELCOME) { } else if($newsletter->type === Newsletter::TYPE_WELCOME) {
$newsletter $newsletter
->withOptions()
->withTotalSent()
->withStatistics(); ->withStatistics();
$options = $newsletter->options()->findArray();
$newsletter->options = Helpers::arrayColumn($options, 'value', 'name');
} else if($newsletter->type === Newsletter::TYPE_NOTIFICATION) { } else if($newsletter->type === Newsletter::TYPE_NOTIFICATION) {
$newsletter $newsletter
->withSegments() ->withSegments()

View File

@ -14,6 +14,7 @@
var mailpoet_schedule_time_of_day = <%= json_encode(schedule_time_of_day) %>; var mailpoet_schedule_time_of_day = <%= json_encode(schedule_time_of_day) %>;
var mailpoet_date_display_format = "<%= wp_date_format() %>"; var mailpoet_date_display_format = "<%= wp_date_format() %>";
var mailpoet_date_storage_format = "Y-m-d"; var mailpoet_date_storage_format = "Y-m-d";
var mailpoet_tracking_enabled = <%= json_encode(tracking_enabled) %>;
</script> </script>
<% endblock %> <% endblock %>
@ -61,6 +62,8 @@
'statistics': __('Opened, Clicked, Unsubscribed'), 'statistics': __('Opened, Clicked, Unsubscribed'),
'lists': __('Lists'), 'lists': __('Lists'),
'settings': __('Settings'), 'settings': __('Settings'),
'history': __('History'),
'viewHistory': __('View history'),
'createdOn': __('Created on'), 'createdOn': __('Created on'),
'lastModifiedOn': __('Last modified on'), 'lastModifiedOn': __('Last modified on'),
'oneNewsletterTrashed': __('1 newsletter was moved to the trash.'), 'oneNewsletterTrashed': __('1 newsletter was moved to the trash.'),
@ -79,6 +82,7 @@
'active': __('Active'), 'active': __('Active'),
'inactive': __('Not Active'), 'inactive': __('Not Active'),
'newsletterQueueCompleted': __('Sent to %$1d of %$2d.'), 'newsletterQueueCompleted': __('Sent to %$1d of %$2d.'),
'sentToXSubscribers': __('Sent to %$1d subscribers.'),
'resume': __('Resume'), 'resume': __('Resume'),
'pause': __('Pause'), 'pause': __('Pause'),
'new': __('New'), 'new': __('New'),
@ -133,7 +137,7 @@
'selectEventToSendWelcomeEmail': __('Select an event to send this welcome email'), 'selectEventToSendWelcomeEmail': __('Select an event to send this welcome email'),
'onSubscriptionToList': __('When someone subscribes to the list...'), 'onSubscriptionToList': __('When someone subscribes to the list...'),
'onWordpressUserRegistration': __('When a new Wordrpess user is added to your site...'), 'onWordpressUserRegistration': __('When a new WordPress user is added to your site...'),
'delayImmediately': __('immediately'), 'delayImmediately': __('immediately'),
'delayHoursAfter': __('hour(s) after'), 'delayHoursAfter': __('hour(s) after'),
'delayDaysAfter': __('day(s) after'), 'delayDaysAfter': __('day(s) after'),