From fbc0a3ad8d0f25f4f95f007401c73e7c5e8371a7 Mon Sep 17 00:00:00 2001 From: Alexey Stoletniy Date: Tue, 4 Apr 2017 15:37:16 +0300 Subject: [PATCH] Add detailed stats page support in Free [PREMIUM-1] --- assets/js/src/listing/listing.jsx | 11 ++++-- assets/js/src/newsletters/listings/mixins.jsx | 35 +++++++++++++------ .../js/src/newsletters/listings/welcome.jsx | 34 ++++++++++++------ assets/js/src/newsletters/newsletters.jsx | 6 ++++ lib/Config/Migrator.php | 6 +++- lib/Models/Newsletter.php | 3 +- tests/unit/Statistics/Track/OpensTest.php | 2 -- webpack.config.js | 13 +++++-- 8 files changed, 82 insertions(+), 28 deletions(-) diff --git a/assets/js/src/listing/listing.jsx b/assets/js/src/listing/listing.jsx index 2c28bf5cbb..3cf9b7799d 100644 --- a/assets/js/src/listing/listing.jsx +++ b/assets/js/src/listing/listing.jsx @@ -209,8 +209,8 @@ const ListingItems = React.createClass({ className="colspanchange"> { (this.props.loading === true) - ? MailPoet.I18n.t('loadingItems') - : MailPoet.I18n.t('noItemsFound') + ? (this.props.messages.onLoadingItems || MailPoet.I18n.t('loadingItems')) + : (this.props.messages.onNoItemsFound || MailPoet.I18n.t('noItemsFound')) } @@ -793,6 +793,12 @@ const Listing = React.createClass({ groups = false; } + // messages + let messages = {}; + if (this.props.messages !== undefined) { + messages = this.props.messages; + } + return (
{ groups } @@ -846,6 +852,7 @@ const Listing = React.createClass({ count={ this.state.count } limit={ this.state.limit } item_actions={ item_actions } + messages={ messages } items={ items } /> diff --git a/assets/js/src/newsletters/listings/mixins.jsx b/assets/js/src/newsletters/listings/mixins.jsx index 98555a8815..1249c52ade 100644 --- a/assets/js/src/newsletters/listings/mixins.jsx +++ b/assets/js/src/newsletters/listings/mixins.jsx @@ -1,9 +1,11 @@ import React from 'react' import ReactDOM from 'react-dom' import ReactStringReplace from 'react-string-replace' +import { Link } from 'react-router' import MailPoet from 'mailpoet' import classNames from 'classnames' import jQuery from 'jquery' +import Hooks from 'wp-js-hooks' const _QueueMixin = { pauseSending: function(newsletter) { @@ -146,6 +148,9 @@ const _StatisticsMixin = { && newsletter.queue && newsletter.queue.status !== 'scheduled' ) { + let params = {}; + params = Hooks.applyFilters('mailpoet_newsletters_listing_stats_before', params, newsletter); + const total_sent = ~~(newsletter.queue.count_processed); let percentage_clicked = 0; @@ -153,22 +158,32 @@ const _StatisticsMixin = { let percentage_unsubscribed = 0; if (total_sent > 0) { - percentage_clicked = Math.round( - (~~(newsletter.statistics.clicked) * 100) / total_sent - ); - percentage_opened = Math.round( - (~~(newsletter.statistics.opened) * 100) / total_sent - ); - percentage_unsubscribed = Math.round( - (~~(newsletter.statistics.unsubscribed) * 100) / total_sent - ); + percentage_clicked = (newsletter.statistics.clicked * 100) / total_sent; + percentage_opened = (newsletter.statistics.opened * 100) / total_sent; + percentage_unsubscribed = (newsletter.statistics.unsubscribed * 100) / total_sent; } - return ( + // format to 1 decimal place + percentage_clicked = percentage_clicked.toFixed(1); + percentage_opened = percentage_opened.toFixed(1); + percentage_unsubscribed = percentage_unsubscribed.toFixed(1); + + const content = ( { percentage_opened }%, { percentage_clicked }%, { percentage_unsubscribed }% ); + + if (total_sent > 0 && params.link) { + return ( + { content } + ); + } + + return content; } else { return ( {MailPoet.I18n.t('notSentYet')} diff --git a/assets/js/src/newsletters/listings/welcome.jsx b/assets/js/src/newsletters/listings/welcome.jsx index ac4d269089..114aaa6d76 100644 --- a/assets/js/src/newsletters/listings/welcome.jsx +++ b/assets/js/src/newsletters/listings/welcome.jsx @@ -11,6 +11,7 @@ import classNames from 'classnames' import jQuery from 'jquery' import MailPoet from 'mailpoet' import _ from 'underscore' +import Hooks from 'wp-js-hooks' const mailpoet_roles = window.mailpoet_roles || {}; const mailpoet_segments = window.mailpoet_segments || {}; @@ -281,24 +282,37 @@ const NewsletterListWelcome = React.createClass({ return; } + let params = {}; + params = Hooks.applyFilters('mailpoet_newsletters_listing_stats_before', params, newsletter); + if (newsletter.total_sent > 0 && newsletter.statistics) { const total_sent = ~~(newsletter.total_sent); - 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 - ); + let percentage_clicked = (newsletter.statistics.clicked * 100) / total_sent; + let percentage_opened = (newsletter.statistics.opened * 100) / total_sent; + let percentage_unsubscribed = (newsletter.statistics.unsubscribed * 100) / total_sent; - return ( + // format to 1 decimal place + percentage_clicked = percentage_clicked.toFixed(1); + percentage_opened = percentage_opened.toFixed(1); + percentage_unsubscribed = percentage_unsubscribed.toFixed(1); + + const content = ( { percentage_opened }%, { percentage_clicked }%, { percentage_unsubscribed }% ); + + if (params.link) { + return ( + { content } + ); + } + + return content; } else { return ( {MailPoet.I18n.t('notSentYet')} diff --git a/assets/js/src/newsletters/newsletters.jsx b/assets/js/src/newsletters/newsletters.jsx index fd82973843..e03772c039 100644 --- a/assets/js/src/newsletters/newsletters.jsx +++ b/assets/js/src/newsletters/newsletters.jsx @@ -2,6 +2,7 @@ import React from 'react' import ReactDOM from 'react-dom' import { Router, Route, IndexRedirect, Link, useRouterHistory } from 'react-router' import { createHashHistory } from 'history' +import Hooks from 'wp-js-hooks' import NewsletterTypes from 'newsletters/types.jsx' import NewsletterTemplates from 'newsletters/templates.jsx' @@ -27,6 +28,9 @@ const App = React.createClass({ const container = document.getElementById('newsletters_container'); if(container) { + let extra_routes = []; + extra_routes = Hooks.applyFilters('mailpoet_newsletters_before_router', extra_routes); + const mailpoet_listing = ReactDOM.render(( @@ -46,6 +50,8 @@ if(container) { {/* Sending options */} + {/* Extra routes */} + { extra_routes.map(rt => ) } ), container); diff --git a/lib/Config/Migrator.php b/lib/Config/Migrator.php index 6e8275dede..9d4a2509c6 100644 --- a/lib/Config/Migrator.php +++ b/lib/Config/Migrator.php @@ -300,7 +300,8 @@ class Migrator { 'subscriber_id mediumint(9) NOT NULL,', 'queue_id mediumint(9) NOT NULL,', 'sent_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,', - 'PRIMARY KEY (id)', + 'PRIMARY KEY (id),', + 'KEY newsletter_id (newsletter_id)', ); return $this->sqlify(__FUNCTION__, $attributes); } @@ -316,6 +317,7 @@ class Migrator { 'created_at TIMESTAMP NULL,', 'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,', 'PRIMARY KEY (id),', + 'KEY newsletter_id (newsletter_id),', 'KEY queue_id (queue_id)', ); return $this->sqlify(__FUNCTION__, $attributes); @@ -329,6 +331,7 @@ class Migrator { 'queue_id mediumint(9) NOT NULL,', 'created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,', 'PRIMARY KEY (id),', + 'KEY newsletter_id (newsletter_id),', 'KEY queue_id (queue_id)', ); return $this->sqlify(__FUNCTION__, $attributes); @@ -342,6 +345,7 @@ class Migrator { 'queue_id mediumint(9) NOT NULL,', 'created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,', 'PRIMARY KEY (id),', + 'KEY newsletter_id (newsletter_id),', 'KEY queue_id (queue_id)', ); return $this->sqlify(__FUNCTION__, $attributes); diff --git a/lib/Models/Newsletter.php b/lib/Models/Newsletter.php index 83e6b766f7..3f42f9136b 100644 --- a/lib/Models/Newsletter.php +++ b/lib/Models/Newsletter.php @@ -264,7 +264,8 @@ class Newsletter extends Model { 'status', 'count_processed', 'count_total', - 'scheduled_at' + 'scheduled_at', + 'created_at' )); if($queue === false) { $this->queue = false; diff --git a/tests/unit/Statistics/Track/OpensTest.php b/tests/unit/Statistics/Track/OpensTest.php index 13b5d12405..4fa4d90501 100644 --- a/tests/unit/Statistics/Track/OpensTest.php +++ b/tests/unit/Statistics/Track/OpensTest.php @@ -3,7 +3,6 @@ use Codeception\Util\Stub; use MailPoet\Models\Newsletter; use MailPoet\Models\SendingQueue; -use MailPoet\Models\StatisticsClicks; use MailPoet\Models\StatisticsOpens; use MailPoet\Models\Subscriber; use MailPoet\Statistics\Track\Opens; @@ -90,6 +89,5 @@ class OpensTest extends MailPoetTest { ORM::raw_execute('TRUNCATE ' . Subscriber::$_table); ORM::raw_execute('TRUNCATE ' . SendingQueue::$_table); ORM::raw_execute('TRUNCATE ' . StatisticsOpens::$_table); - ORM::raw_execute('TRUNCATE ' . StatisticsClicks::$_table); } } \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 42312f945a..d85fc01884 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -65,7 +65,11 @@ baseConfig = { include: require.resolve('react'), loader: 'expose-loader?' + globalPrefix + '.React', }, - { + { + include: require.resolve('react-router'), + loader: 'expose-loader?' + globalPrefix + '.ReactRouter', + }, + { include: require.resolve('react-string-replace'), loader: 'expose-loader?' + globalPrefix + '.ReactStringReplace', }, @@ -73,6 +77,10 @@ baseConfig = { test: /wp-js-hooks/i, loader: 'expose-loader?' + globalPrefix + '.Hooks!exports-loader?wp.hooks', }, + { + test: /listing.jsx/i, + loader: 'expose-loader?' + globalPrefix + '.Listing!babel-loader', + }, { include: /Blob.js$/, loader: 'exports-loader?window.Blob', @@ -124,7 +132,8 @@ config.push(_.extend({}, baseConfig, { 'react', 'react-dom', 'react-router', - 'react-string-replace' + 'react-string-replace', + 'listing/listing.jsx' ], admin: [ 'subscribers/subscribers.jsx',