Add detailed stats page support in Free [PREMIUM-1]

This commit is contained in:
Alexey Stoletniy
2017-04-04 15:37:16 +03:00
parent afedc409f5
commit fbc0a3ad8d
8 changed files with 82 additions and 28 deletions

View File

@ -209,8 +209,8 @@ const ListingItems = React.createClass({
className="colspanchange"> className="colspanchange">
{ {
(this.props.loading === true) (this.props.loading === true)
? MailPoet.I18n.t('loadingItems') ? (this.props.messages.onLoadingItems || MailPoet.I18n.t('loadingItems'))
: MailPoet.I18n.t('noItemsFound') : (this.props.messages.onNoItemsFound || MailPoet.I18n.t('noItemsFound'))
} }
</td> </td>
</tr> </tr>
@ -793,6 +793,12 @@ const Listing = React.createClass({
groups = false; groups = false;
} }
// messages
let messages = {};
if (this.props.messages !== undefined) {
messages = this.props.messages;
}
return ( return (
<div> <div>
{ groups } { groups }
@ -846,6 +852,7 @@ const Listing = React.createClass({
count={ this.state.count } count={ this.state.count }
limit={ this.state.limit } limit={ this.state.limit }
item_actions={ item_actions } item_actions={ item_actions }
messages={ messages }
items={ items } /> items={ items } />
<tfoot> <tfoot>

View File

@ -1,9 +1,11 @@
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import ReactStringReplace from 'react-string-replace' import ReactStringReplace from 'react-string-replace'
import { Link } from 'react-router'
import MailPoet from 'mailpoet' import MailPoet from 'mailpoet'
import classNames from 'classnames' import classNames from 'classnames'
import jQuery from 'jquery' import jQuery from 'jquery'
import Hooks from 'wp-js-hooks'
const _QueueMixin = { const _QueueMixin = {
pauseSending: function(newsletter) { pauseSending: function(newsletter) {
@ -146,6 +148,9 @@ const _StatisticsMixin = {
&& newsletter.queue && newsletter.queue
&& newsletter.queue.status !== 'scheduled' && newsletter.queue.status !== 'scheduled'
) { ) {
let params = {};
params = Hooks.applyFilters('mailpoet_newsletters_listing_stats_before', params, newsletter);
const total_sent = ~~(newsletter.queue.count_processed); const total_sent = ~~(newsletter.queue.count_processed);
let percentage_clicked = 0; let percentage_clicked = 0;
@ -153,22 +158,32 @@ const _StatisticsMixin = {
let percentage_unsubscribed = 0; let percentage_unsubscribed = 0;
if (total_sent > 0) { if (total_sent > 0) {
percentage_clicked = Math.round( percentage_clicked = (newsletter.statistics.clicked * 100) / total_sent;
(~~(newsletter.statistics.clicked) * 100) / total_sent percentage_opened = (newsletter.statistics.opened * 100) / total_sent;
); percentage_unsubscribed = (newsletter.statistics.unsubscribed * 100) / total_sent;
percentage_opened = Math.round(
(~~(newsletter.statistics.opened) * 100) / total_sent
);
percentage_unsubscribed = Math.round(
(~~(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 = (
<span> <span>
{ percentage_opened }%, { percentage_clicked }%, { percentage_unsubscribed }% { percentage_opened }%, { percentage_clicked }%, { percentage_unsubscribed }%
</span> </span>
); );
if (total_sent > 0 && params.link) {
return (
<Link
key={ `stats-${newsletter.id}` }
to={ params.link }
>{ content }</Link>
);
}
return content;
} else { } else {
return ( return (
<span>{MailPoet.I18n.t('notSentYet')}</span> <span>{MailPoet.I18n.t('notSentYet')}</span>

View File

@ -11,6 +11,7 @@ import classNames from 'classnames'
import jQuery from 'jquery' import jQuery from 'jquery'
import MailPoet from 'mailpoet' import MailPoet from 'mailpoet'
import _ from 'underscore' import _ from 'underscore'
import Hooks from 'wp-js-hooks'
const mailpoet_roles = window.mailpoet_roles || {}; const mailpoet_roles = window.mailpoet_roles || {};
const mailpoet_segments = window.mailpoet_segments || {}; const mailpoet_segments = window.mailpoet_segments || {};
@ -281,24 +282,37 @@ const NewsletterListWelcome = React.createClass({
return; return;
} }
let params = {};
params = Hooks.applyFilters('mailpoet_newsletters_listing_stats_before', params, newsletter);
if (newsletter.total_sent > 0 && newsletter.statistics) { if (newsletter.total_sent > 0 && newsletter.statistics) {
const total_sent = ~~(newsletter.total_sent); const total_sent = ~~(newsletter.total_sent);
const percentage_clicked = Math.round( let percentage_clicked = (newsletter.statistics.clicked * 100) / total_sent;
(~~(newsletter.statistics.clicked) * 100) / total_sent let percentage_opened = (newsletter.statistics.opened * 100) / total_sent;
); let percentage_unsubscribed = (newsletter.statistics.unsubscribed * 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 ( // 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 = (
<span> <span>
{ percentage_opened }%, { percentage_clicked }%, { percentage_unsubscribed }% { percentage_opened }%, { percentage_clicked }%, { percentage_unsubscribed }%
</span> </span>
); );
if (params.link) {
return (
<Link
key={ `stats-${newsletter.id}` }
to={ params.link }
>{ content }</Link>
);
}
return content;
} else { } else {
return ( return (
<span>{MailPoet.I18n.t('notSentYet')}</span> <span>{MailPoet.I18n.t('notSentYet')}</span>

View File

@ -2,6 +2,7 @@ import React from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import { Router, Route, IndexRedirect, Link, useRouterHistory } from 'react-router' import { Router, Route, IndexRedirect, Link, useRouterHistory } from 'react-router'
import { createHashHistory } from 'history' import { createHashHistory } from 'history'
import Hooks from 'wp-js-hooks'
import NewsletterTypes from 'newsletters/types.jsx' import NewsletterTypes from 'newsletters/types.jsx'
import NewsletterTemplates from 'newsletters/templates.jsx' import NewsletterTemplates from 'newsletters/templates.jsx'
@ -27,6 +28,9 @@ const App = React.createClass({
const container = document.getElementById('newsletters_container'); const container = document.getElementById('newsletters_container');
if(container) { if(container) {
let extra_routes = [];
extra_routes = Hooks.applyFilters('mailpoet_newsletters_before_router', extra_routes);
const mailpoet_listing = ReactDOM.render(( const mailpoet_listing = ReactDOM.render((
<Router history={ history }> <Router history={ history }>
<Route path="/" component={ App }> <Route path="/" component={ App }>
@ -46,6 +50,8 @@ if(container) {
<Route name="template" path="template/:id" component={ NewsletterTemplates } /> <Route name="template" path="template/:id" component={ NewsletterTemplates } />
{/* Sending options */} {/* Sending options */}
<Route path="send/:id" component={ NewsletterSend } /> <Route path="send/:id" component={ NewsletterSend } />
{/* Extra routes */}
{ extra_routes.map(rt => <Route key={rt.path} path={rt.path} component={rt.component} />) }
</Route> </Route>
</Router> </Router>
), container); ), container);

View File

@ -300,7 +300,8 @@ class Migrator {
'subscriber_id mediumint(9) NOT NULL,', 'subscriber_id mediumint(9) NOT NULL,',
'queue_id mediumint(9) NOT NULL,', 'queue_id mediumint(9) NOT NULL,',
'sent_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,', '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); return $this->sqlify(__FUNCTION__, $attributes);
} }
@ -316,6 +317,7 @@ class Migrator {
'created_at TIMESTAMP NULL,', 'created_at TIMESTAMP NULL,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,', 'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),', 'PRIMARY KEY (id),',
'KEY newsletter_id (newsletter_id),',
'KEY queue_id (queue_id)', 'KEY queue_id (queue_id)',
); );
return $this->sqlify(__FUNCTION__, $attributes); return $this->sqlify(__FUNCTION__, $attributes);
@ -329,6 +331,7 @@ class Migrator {
'queue_id mediumint(9) NOT NULL,', 'queue_id mediumint(9) NOT NULL,',
'created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,', 'created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),', 'PRIMARY KEY (id),',
'KEY newsletter_id (newsletter_id),',
'KEY queue_id (queue_id)', 'KEY queue_id (queue_id)',
); );
return $this->sqlify(__FUNCTION__, $attributes); return $this->sqlify(__FUNCTION__, $attributes);
@ -342,6 +345,7 @@ class Migrator {
'queue_id mediumint(9) NOT NULL,', 'queue_id mediumint(9) NOT NULL,',
'created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,', 'created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),', 'PRIMARY KEY (id),',
'KEY newsletter_id (newsletter_id),',
'KEY queue_id (queue_id)', 'KEY queue_id (queue_id)',
); );
return $this->sqlify(__FUNCTION__, $attributes); return $this->sqlify(__FUNCTION__, $attributes);

View File

@ -264,7 +264,8 @@ class Newsletter extends Model {
'status', 'status',
'count_processed', 'count_processed',
'count_total', 'count_total',
'scheduled_at' 'scheduled_at',
'created_at'
)); ));
if($queue === false) { if($queue === false) {
$this->queue = false; $this->queue = false;

View File

@ -3,7 +3,6 @@
use Codeception\Util\Stub; use Codeception\Util\Stub;
use MailPoet\Models\Newsletter; use MailPoet\Models\Newsletter;
use MailPoet\Models\SendingQueue; use MailPoet\Models\SendingQueue;
use MailPoet\Models\StatisticsClicks;
use MailPoet\Models\StatisticsOpens; use MailPoet\Models\StatisticsOpens;
use MailPoet\Models\Subscriber; use MailPoet\Models\Subscriber;
use MailPoet\Statistics\Track\Opens; use MailPoet\Statistics\Track\Opens;
@ -90,6 +89,5 @@ class OpensTest extends MailPoetTest {
ORM::raw_execute('TRUNCATE ' . Subscriber::$_table); ORM::raw_execute('TRUNCATE ' . Subscriber::$_table);
ORM::raw_execute('TRUNCATE ' . SendingQueue::$_table); ORM::raw_execute('TRUNCATE ' . SendingQueue::$_table);
ORM::raw_execute('TRUNCATE ' . StatisticsOpens::$_table); ORM::raw_execute('TRUNCATE ' . StatisticsOpens::$_table);
ORM::raw_execute('TRUNCATE ' . StatisticsClicks::$_table);
} }
} }

View File

@ -65,7 +65,11 @@ baseConfig = {
include: require.resolve('react'), include: require.resolve('react'),
loader: 'expose-loader?' + globalPrefix + '.React', loader: 'expose-loader?' + globalPrefix + '.React',
}, },
{ {
include: require.resolve('react-router'),
loader: 'expose-loader?' + globalPrefix + '.ReactRouter',
},
{
include: require.resolve('react-string-replace'), include: require.resolve('react-string-replace'),
loader: 'expose-loader?' + globalPrefix + '.ReactStringReplace', loader: 'expose-loader?' + globalPrefix + '.ReactStringReplace',
}, },
@ -73,6 +77,10 @@ baseConfig = {
test: /wp-js-hooks/i, test: /wp-js-hooks/i,
loader: 'expose-loader?' + globalPrefix + '.Hooks!exports-loader?wp.hooks', loader: 'expose-loader?' + globalPrefix + '.Hooks!exports-loader?wp.hooks',
}, },
{
test: /listing.jsx/i,
loader: 'expose-loader?' + globalPrefix + '.Listing!babel-loader',
},
{ {
include: /Blob.js$/, include: /Blob.js$/,
loader: 'exports-loader?window.Blob', loader: 'exports-loader?window.Blob',
@ -124,7 +132,8 @@ config.push(_.extend({}, baseConfig, {
'react', 'react',
'react-dom', 'react-dom',
'react-router', 'react-router',
'react-string-replace' 'react-string-replace',
'listing/listing.jsx'
], ],
admin: [ admin: [
'subscribers/subscribers.jsx', 'subscribers/subscribers.jsx',