diff --git a/assets/js/src/listing/listing.jsx b/assets/js/src/listing/listing.jsx index 0dac115b94..a21fa7ec95 100644 --- a/assets/js/src/listing/listing.jsx +++ b/assets/js/src/listing/listing.jsx @@ -676,6 +676,12 @@ define( sort_by = this.state.sort_by, 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 var bulk_actions = this.props.bulk_actions || []; @@ -761,7 +767,7 @@ define( selection={ this.state.selection } sort_by={ sort_by } sort_order={ sort_order } - columns={ this.props.columns } + columns={ columns } is_selectable={ bulk_actions.length > 0 } /> @@ -771,7 +777,7 @@ define( onRestoreItem={ this.handleRestoreItem } onTrashItem={ this.handleTrashItem } onRefreshItems={ this.handleRefreshItems } - columns={ this.props.columns } + columns={ columns } is_selectable={ bulk_actions.length > 0 } onSelectItem={ this.handleSelectItem } onSelectAll={ this.handleSelectAll } @@ -791,7 +797,7 @@ define( selection={ this.state.selection } sort_by={ sort_by } sort_order={ sort_order } - columns={ this.props.columns } + columns={ columns } is_selectable={ bulk_actions.length > 0 } /> diff --git a/assets/js/src/newsletters/listings/notification.jsx b/assets/js/src/newsletters/listings/notification.jsx index 69f51ed4da..381ab0c869 100644 --- a/assets/js/src/newsletters/listings/notification.jsx +++ b/assets/js/src/newsletters/listings/notification.jsx @@ -68,17 +68,12 @@ var columns = [ label: MailPoet.I18n.t('status') }, { - name: 'segments', - label: MailPoet.I18n.t('lists') + name: 'settings', + label: MailPoet.I18n.t('settings') }, { - name: 'statistics', - label: MailPoet.I18n.t('statistics') - }, - { - name: 'created_at', - label: MailPoet.I18n.t('createdOn'), - sortable: true + name: 'history', + label: MailPoet.I18n.t('history') }, { name: 'updated_at', @@ -96,6 +91,16 @@ var bulk_actions = [ ]; var newsletter_actions = [ + { + name: 'view', + link: function(newsletter) { + return ( + + {MailPoet.I18n.t('preview')} + + ); + } + }, { name: 'edit', 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' } @@ -158,31 +183,6 @@ const NewsletterListNotification = React.createClass({ ); }, - renderStatistics: function(newsletter) { - if (!newsletter.statistics || !newsletter.queue || newsletter.queue.count_processed == 0 || newsletter.queue.status === 'scheduled') { - return ( - - {MailPoet.I18n.t('notSentYet')} - - ); - } - - 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 ( - - { percentage_opened }%, { percentage_clicked }%, { percentage_unsubscribed }% - - ); - }, renderItem: function(newsletter, actions) { var rowClasses = classNames( 'manage-column', @@ -190,10 +190,6 @@ const NewsletterListNotification = React.createClass({ 'has-row-actions' ); - var segments = newsletter.segments.map(function(segment) { - return segment.name - }).join(', '); - return (
@@ -207,14 +203,11 @@ const NewsletterListNotification = React.createClass({ { this.renderStatus(newsletter) } - - { segments } + + { this.renderSettings(newsletter) } - - { this.renderStatistics(newsletter) } - - - { MailPoet.Date.format(newsletter.created_at) } + + { MailPoet.I18n.t('viewHistory') } { MailPoet.Date.format(newsletter.updated_at) } diff --git a/assets/js/src/newsletters/listings/standard.jsx b/assets/js/src/newsletters/listings/standard.jsx index 78ad5201c8..98e2f8b985 100644 --- a/assets/js/src/newsletters/listings/standard.jsx +++ b/assets/js/src/newsletters/listings/standard.jsx @@ -9,6 +9,8 @@ import classNames from 'classnames' import jQuery from 'jquery' import MailPoet from 'mailpoet' +const mailpoet_tracking_enabled = (!!(window['mailpoet_tracking_enabled'])); + const messages = { onTrash(response) { const count = ~~response; @@ -73,7 +75,8 @@ var columns = [ }, { name: 'statistics', - label: MailPoet.I18n.t('statistics') + label: MailPoet.I18n.t('statistics'), + display: mailpoet_tracking_enabled }, { name: 'updated_at', @@ -82,6 +85,7 @@ var columns = [ } ]; + var bulk_actions = [ { name: 'trash', @@ -231,30 +235,33 @@ const NewsletterListStandard = React.createClass({ ); } }, - renderStatistics: function(item) { - if(!item.statistics || !item.queue || item.queue.count_processed == 0 || item.queue.status === 'scheduled') { - return ( - - {MailPoet.I18n.t('notSentYet')} - - ); + renderStatistics: function(newsletter) { + if (mailpoet_tracking_enabled === false) { + return; } - var percentage_clicked = Math.round( - (item.statistics.clicked * 100) / (item.queue.count_processed) - ); - var percentage_opened = Math.round( - (item.statistics.opened * 100) / (item.queue.count_processed) - ); - var percentage_unsubscribed = Math.round( - (item.statistics.unsubscribed * 100) / (item.queue.count_processed) - ); + if(newsletter.statistics && newsletter.queue && newsletter.queue.status !== 'scheduled') { + const total_sent = ~~(newsletter.queue.count_processed); + const percentage_clicked = Math.round( + (~~(newsletter.statistics.clicked) * 100) / total_sent + ); + const percentage_opened = Math.round( + (~~(newsletter.statistics.opened) * 100) / total_sent + ); + const percentage_unsubscribed = Math.round( + (~~(newsletter.statistics.unsubscribed) * 100) / total_sent + ); - return ( - - { percentage_opened }%, { percentage_clicked }%, { percentage_unsubscribed }% - - ); + return ( + + { percentage_opened }%, { percentage_clicked }%, { percentage_unsubscribed }% + + ); + } else { + return ( + {MailPoet.I18n.t('notSentYet')} + ); + } }, renderItem: function(newsletter, actions) { var rowClasses = classNames( @@ -283,9 +290,11 @@ const NewsletterListStandard = React.createClass({ { segments } - - { this.renderStatistics(newsletter) } - + {(mailpoet_tracking_enabled === true) ? ( + + { this.renderStatistics(newsletter) } + + ) : null } { MailPoet.Date.format(newsletter.updated_at) } diff --git a/assets/js/src/newsletters/listings/welcome.jsx b/assets/js/src/newsletters/listings/welcome.jsx index a8ed7aee96..0c53af1e96 100644 --- a/assets/js/src/newsletters/listings/welcome.jsx +++ b/assets/js/src/newsletters/listings/welcome.jsx @@ -8,6 +8,7 @@ import ListingTabs from 'newsletters/listings/tabs.jsx' import classNames from 'classnames' import jQuery from 'jquery' import MailPoet from 'mailpoet' +import _ from 'underscore' const messages = { onTrash(response) { @@ -165,21 +166,48 @@ const NewsletterListWelcome = React.createClass({ }.bind(this)); }, renderStatus: function(newsletter) { + let total_sent; + total_sent = ( + MailPoet.I18n.t('sentToXSubscribers') + .replace('%$1d', newsletter.total_sent) + ); + return ( - +
+ +

{ total_sent }

+
); }, 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 ( - Settings... + { settings } ); }, diff --git a/lib/Config/Menu.php b/lib/Config/Menu.php index 79bc7dafc8..d9f2361ca3 100644 --- a/lib/Config/Menu.php +++ b/lib/Config/Menu.php @@ -413,6 +413,8 @@ class Menu { 24 ); + $data['tracking_enabled'] = Setting::getValue('tracking.enabled'); + wp_enqueue_script('jquery-ui'); wp_enqueue_script('jquery-ui-datepicker'); diff --git a/lib/Cron/Workers/SendingQueue.php b/lib/Cron/Workers/SendingQueue.php index 819a9bf519..6f52ebfba1 100644 --- a/lib/Cron/Workers/SendingQueue.php +++ b/lib/Cron/Workers/SendingQueue.php @@ -4,6 +4,7 @@ namespace MailPoet\Cron\Workers; use MailPoet\Cron\CronHelper; use MailPoet\Mailer\Mailer; use MailPoet\Models\Newsletter; +use MailPoet\Models\SendingQueue as SendingQueueModel; use MailPoet\Models\NewsletterPost; use MailPoet\Models\Setting; use MailPoet\Models\StatisticsNewsletters; @@ -23,7 +24,6 @@ class SendingQueue { private $timer; const BATCH_SIZE = 50; const DIVIDER = '***MailPoet***'; - const STATUS_COMPLETED = 'completed'; function __construct($timer = false) { $this->mta_config = $this->getMailerConfig(); @@ -295,7 +295,7 @@ class SendingQueue { } function getQueues() { - return \MailPoet\Models\SendingQueue::orderByDesc('priority') + return SendingQueueModel::orderByDesc('priority') ->whereNull('deleted_at') ->whereNull('status') ->findResultSet(); @@ -321,7 +321,7 @@ class SendingQueue { $queue->count_processed + $queue->count_to_process; if(!$queue->count_to_process) { $queue->processed_at = current_time('mysql'); - $queue->status = self::STATUS_COMPLETED; + $queue->status = SendingQueueModel::STATUS_COMPLETED; } $queue->subscribers = serialize((array) $queue->subscribers); $queue->save(); diff --git a/lib/Models/Newsletter.php b/lib/Models/Newsletter.php index 4f4d6a2653..6732d0e0d1 100644 --- a/lib/Models/Newsletter.php +++ b/lib/Models/Newsletter.php @@ -1,5 +1,6 @@ asArray(); - unset($data['id']); + // get current newsletter's data as an array + $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); + + // reset timestamps $duplicate->set_expr('created_at', 'NOW()'); $duplicate->set_expr('updated_at', 'NOW()'); $duplicate->set_expr('deleted_at', 'NULL'); @@ -67,15 +76,25 @@ class Newsletter extends Model { // reset status $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()) { - return $duplicate; - } else { - return false; + if(!empty($segments)) { + foreach($segments as $segment) { + $relation = NewsletterSegment::create(); + $relation->segment_id = $segment['id']; + $relation->newsletter_id = $duplicate->id(); + $result = $relation->save(); + } + } + + // TODO: duplicate options (if need be) } + + return $duplicate; } function asArray() { @@ -133,6 +152,24 @@ class Newsletter extends Model { 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() { $statistics = $this->getStatistics(); if($statistics === false) { @@ -140,6 +177,7 @@ class Newsletter extends Model { } else { $this->statistics = $statistics->asArray(); } + return $this; } @@ -149,9 +187,9 @@ class Newsletter extends Model { } return SendingQueue::tableAlias('queues') ->selectExpr( - 'count(DISTINCT(clicks.subscriber_id)) as clicked, ' . - 'count(DISTINCT(opens.subscriber_id)) as opened, ' . - 'count(DISTINCT(unsubscribes.subscriber_id)) as unsubscribed ' + 'COUNT(DISTINCT(clicks.subscriber_id)) as clicked, ' . + 'COUNT(DISTINCT(opens.subscriber_id)) as opened, ' . + 'COUNT(DISTINCT(unsubscribes.subscriber_id)) as unsubscribed ' ) ->leftOuterJoin( MP_STATISTICS_CLICKS_TABLE, @@ -299,14 +337,6 @@ class Newsletter extends Model { case self::TYPE_WELCOME: case self::TYPE_NOTIFICATION: $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( 'name' => self::STATUS_ACTIVE, 'label' => __('Active'), @@ -314,6 +344,14 @@ class Newsletter extends Model { ->filter('filterType', $type) ->filter('filterStatus', self::STATUS_ACTIVE) ->count() + ), + array( + 'name' => self::STATUS_DRAFT, + 'label' => __('Not active'), + 'count' => Newsletter::getPublished() + ->filter('filterType', $type) + ->filter('filterStatus', self::STATUS_DRAFT) + ->count() ) )); break; @@ -379,7 +417,15 @@ class Newsletter extends Model { } 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('groupBy', $data) ->filter('search', $data['search']); diff --git a/lib/Models/SendingQueue.php b/lib/Models/SendingQueue.php index c6fd1c7978..b5ea965826 100644 --- a/lib/Models/SendingQueue.php +++ b/lib/Models/SendingQueue.php @@ -6,6 +6,8 @@ if(!defined('ABSPATH')) exit; class SendingQueue extends Model { public static $_table = MP_SENDING_QUEUES_TABLE; + const STATUS_COMPLETED = 'completed'; + function __construct() { parent::__construct(); } diff --git a/lib/Router/Newsletters.php b/lib/Router/Newsletters.php index 18b6610b11..d7b6e1e91e 100644 --- a/lib/Router/Newsletters.php +++ b/lib/Router/Newsletters.php @@ -150,9 +150,13 @@ class Newsletters { $newsletter = Newsletter::findOne($id); if($newsletter !== false) { - $result = $newsletter->duplicate(array( + $duplicate = $newsletter->duplicate(array( 'subject' => sprintf(__('Copy of %s'), $newsletter->subject) - ))->asArray(); + )); + + if($duplicate !== false && $duplicate->getErrors() === false) { + $result = $newsletter->asArray(); + } } return $result; } @@ -256,7 +260,13 @@ class Newsletters { ->withStatistics(); } else if($newsletter->type === Newsletter::TYPE_WELCOME) { $newsletter + ->withOptions() + ->withTotalSent() ->withStatistics(); + + $options = $newsletter->options()->findArray(); + $newsletter->options = Helpers::arrayColumn($options, 'value', 'name'); + } else if($newsletter->type === Newsletter::TYPE_NOTIFICATION) { $newsletter ->withSegments() diff --git a/views/newsletters.html b/views/newsletters.html index 1ab00011fd..6389603dbc 100644 --- a/views/newsletters.html +++ b/views/newsletters.html @@ -14,6 +14,7 @@ var mailpoet_schedule_time_of_day = <%= json_encode(schedule_time_of_day) %>; var mailpoet_date_display_format = "<%= wp_date_format() %>"; var mailpoet_date_storage_format = "Y-m-d"; + var mailpoet_tracking_enabled = <%= json_encode(tracking_enabled) %>; <% endblock %> @@ -61,6 +62,8 @@ 'statistics': __('Opened, Clicked, Unsubscribed'), 'lists': __('Lists'), 'settings': __('Settings'), + 'history': __('History'), + 'viewHistory': __('View history'), 'createdOn': __('Created on'), 'lastModifiedOn': __('Last modified on'), 'oneNewsletterTrashed': __('1 newsletter was moved to the trash.'), @@ -79,6 +82,7 @@ 'active': __('Active'), 'inactive': __('Not Active'), 'newsletterQueueCompleted': __('Sent to %$1d of %$2d.'), + 'sentToXSubscribers': __('Sent to %$1d subscribers.'), 'resume': __('Resume'), 'pause': __('Pause'), 'new': __('New'), @@ -133,7 +137,7 @@ 'selectEventToSendWelcomeEmail': __('Select an event to send this welcome email'), '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'), 'delayHoursAfter': __('hour(s) after'), 'delayDaysAfter': __('day(s) after'),