Merge pull request #552 from mailpoet/newsletter_listing

Post notification history listing
This commit is contained in:
Tautvidas Sipavičius
2016-07-22 16:20:00 +03:00
committed by GitHub
20 changed files with 533 additions and 228 deletions

View File

@ -1,6 +1,7 @@
import MailPoet from 'mailpoet'
import jQuery from 'jquery'
import React from 'react'
import _ from 'underscore'
import { Router, Link } from 'react-router'
import classNames from 'classnames'
import ListingBulkActions from 'listing/bulk_actions.jsx'
@ -13,7 +14,7 @@ import ListingFilters from 'listing/filters.jsx'
const ListingItem = React.createClass({
getInitialState: function() {
return {
toggled: true
expanded: false
};
},
handleSelectItem: function(e) {
@ -34,7 +35,7 @@ const ListingItem = React.createClass({
this.props.onDeleteItem(id);
},
handleToggleItem: function(id) {
this.setState({ toggled: !this.state.toggled });
this.setState({ expanded: !this.state.expanded });
},
render: function() {
var checkbox = false;
@ -182,7 +183,7 @@ const ListingItem = React.createClass({
);
}
const row_classes = classNames({ 'is-expanded': !this.state.toggled });
const row_classes = classNames({ 'is-expanded': this.state.expanded });
return (
<tr className={ row_classes }>
@ -303,13 +304,12 @@ const Listing = React.createClass({
getParam: function(param) {
const regex = /(.*)\[(.*)\]/;
const matches = regex.exec(param);
return [matches[1], matches[2]]
return [matches[1], matches[2]];
},
initWithParams: function(params) {
let state = this.getInitialState();
// check for url params
if (params.splat !== undefined) {
if (params.splat) {
params.splat.split('/').map(param => {
let [key, value] = this.getParam(param);
switch(key) {
@ -348,6 +348,17 @@ const Listing = React.createClass({
this.getItems();
}.bind(this));
},
getParams: function() {
// get all route parameters (without the "splat")
let params = _.omit(this.props.params, 'splat');
// TODO:
// find a way to set the "type" in the routes definition
// so that it appears in `this.props.params`
if (this.props.type) {
params.type = this.props.type;
}
return params;
},
setParams: function() {
if (this.props.location) {
let params = Object.keys(this.state)
@ -378,17 +389,37 @@ const Listing = React.createClass({
.filter(key => { return (key !== undefined) })
.join('/');
// prepend url with "tab" if specified
if (this.props.tab !== undefined) {
params = `/${ this.props.tab }/${ params }`;
// set url
let url = this.getUrlWithParams(params);
if (this.props.location.pathname !== url) {
this.context.router.push(`${url}`);
}
}
},
getUrlWithParams: function(params) {
let base_url = (this.props.base_url !== undefined)
? this.props.base_url
: null;
if (base_url !== null) {
base_url = this.setBaseUrlParams(base_url);
return `/${ base_url }/${ params }`;
} else {
params = `/${ params }`;
return `/${ params }`;
}
},
setBaseUrlParams: function(base_url) {
if (base_url.indexOf(':') !== -1) {
const params = this.getParams();
Object.keys(params).map((key) => {
if (base_url.indexOf(':'+key) !== -1) {
base_url = base_url.replace(':'+key, params[key]);
}
});
}
if (this.props.location.pathname !== params) {
this.context.router.push(`${params}`);
}
}
return base_url;
},
componentDidMount: function() {
if (this.isMounted()) {
@ -416,7 +447,7 @@ const Listing = React.createClass({
endpoint: this.props.endpoint,
action: 'listing',
data: {
tab: (this.props.tab) ? this.props.tab : '',
params: this.getParams(),
offset: (this.state.page - 1) * this.state.limit,
limit: this.state.limit,
group: this.state.group,
@ -531,7 +562,7 @@ const Listing = React.createClass({
var data = params || {};
data.listing = {
tab: (this.props.tab) ? this.props.tab : '',
params: this.getParams(),
offset: 0,
limit: 0,
filter: this.state.filter,

View File

@ -0,0 +1,152 @@
import React from 'react'
import MailPoet from 'mailpoet'
import classNames from 'classnames'
import jQuery from 'jquery'
const _QueueMixin = {
pauseSending: function(newsletter) {
MailPoet.Ajax.post({
endpoint: 'sendingQueue',
action: 'pause',
data: newsletter.id
}).done(function() {
jQuery('#resume_'+newsletter.id).show();
jQuery('#pause_'+newsletter.id).hide();
});
},
resumeSending: function(newsletter) {
MailPoet.Ajax.post({
endpoint: 'sendingQueue',
action: 'resume',
data: newsletter.id
}).done(function() {
jQuery('#pause_'+newsletter.id).show();
jQuery('#resume_'+newsletter.id).hide();
});
},
renderQueueStatus: function(newsletter) {
if (!newsletter.queue) {
return (
<span>{MailPoet.I18n.t('notSentYet')}</span>
);
} else {
if (newsletter.queue.status === 'scheduled') {
return (
<span>
{ MailPoet.I18n.t('scheduledFor') } { MailPoet.Date.format(newsletter.queue.scheduled_at) }
</span>
)
}
const progressClasses = classNames(
'mailpoet_progress',
{ 'mailpoet_progress_complete': newsletter.queue.status === 'completed'}
);
// calculate percentage done
const percentage = Math.round(
(newsletter.queue.count_processed * 100) / (newsletter.queue.count_total)
);
let label;
if (newsletter.queue.status === 'completed') {
label = (
<span>
{
MailPoet.I18n.t('newsletterQueueCompleted')
.replace(
"%$1d",
newsletter.queue.count_processed - newsletter.queue.count_failed
)
.replace(
"%$2d",
newsletter.queue.count_total
)
}
</span>
);
} else {
label = (
<span>
{ newsletter.queue.count_processed } / { newsletter.queue.count_total }
&nbsp;&nbsp;
<a
id={ 'resume_'+newsletter.id }
className="button"
style={{ display: (newsletter.queue.status === 'paused')
? 'inline-block': 'none' }}
href="javascript:;"
onClick={ this.resumeSending.bind(null, newsletter) }
>{MailPoet.I18n.t('resume')}</a>
<a
id={ 'pause_'+newsletter.id }
className="button mailpoet_pause"
style={{ display: (newsletter.queue.status === null)
? 'inline-block': 'none' }}
href="javascript:;"
onClick={ this.pauseSending.bind(null, newsletter) }
>{MailPoet.I18n.t('pause')}</a>
</span>
);
}
return (
<div>
<div className={ progressClasses }>
<span
className="mailpoet_progress_bar"
style={ { width: percentage + "%"} }
></span>
<span className="mailpoet_progress_label">
{ percentage + "%" }
</span>
</div>
<p style={{ textAlign:'center' }}>
{ label }
</p>
</div>
);
}
},
};
const _StatisticsMixin = {
renderStatistics: function(newsletter) {
if (
newsletter.statistics
&& newsletter.queue
&& newsletter.queue.status !== 'scheduled'
) {
const total_sent = ~~(newsletter.queue.count_processed);
let percentage_clicked = 0;
let percentage_opened = 0;
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
);
}
return (
<span>
{ percentage_opened }%, { percentage_clicked }%, { percentage_unsubscribed }%
</span>
);
} else {
return (
<span>{MailPoet.I18n.t('notSentYet')}</span>
);
}
}
}
export { _QueueMixin as QueueMixin };
export { _StatisticsMixin as StatisticsMixin };

View File

@ -252,6 +252,20 @@ const NewsletterListNotification = React.createClass({
</span>
);
},
renderHistoryLink: function(newsletter) {
const childrenCount = ~~(newsletter.children_count);
if (childrenCount === 0) {
return (
MailPoet.I18n.t('notSentYet')
);
} else {
return (
<Link
to={ `/notification/history/${ newsletter.id }` }
>{ MailPoet.I18n.t('viewHistory') }</Link>
);
}
},
renderItem: function(newsletter, actions) {
const rowClasses = classNames(
'manage-column',
@ -277,7 +291,7 @@ const NewsletterListNotification = React.createClass({
{ this.renderSettings(newsletter) }
</td>
<td className="column" data-colname={ MailPoet.I18n.t('history') }>
<a href="#TODO">{ MailPoet.I18n.t('viewHistory') }</a>
{ this.renderHistoryLink(newsletter) }
</td>
<td className="column-date" data-colname={ MailPoet.I18n.t('lastModifiedOn') }>
<abbr>{ MailPoet.Date.format(newsletter.updated_at) }</abbr>
@ -299,7 +313,8 @@ const NewsletterListNotification = React.createClass({
location={ this.props.location }
params={ this.props.params }
endpoint="newsletters"
tab="notification"
type="notification"
base_url="notification"
onRenderItem={ this.renderItem }
columns={ columns }
bulk_actions={ bulk_actions }

View File

@ -0,0 +1,125 @@
import React from 'react'
import { Router, Link } from 'react-router'
import classNames from 'classnames'
import jQuery from 'jquery'
import MailPoet from 'mailpoet'
import Listing from 'listing/listing.jsx'
import ListingTabs from 'newsletters/listings/tabs.jsx'
import { QueueMixin, StatisticsMixin } from 'newsletters/listings/mixins.jsx'
const mailpoet_tracking_enabled = (!!(window['mailpoet_tracking_enabled']));
const columns = [
{
name: 'subject',
label: MailPoet.I18n.t('subject'),
},
{
name: 'status',
label: MailPoet.I18n.t('status')
},
{
name: 'segments',
label: MailPoet.I18n.t('lists')
},
{
name: 'statistics',
label: MailPoet.I18n.t('statistics'),
display: mailpoet_tracking_enabled
},
{
name: 'processed_at',
label: MailPoet.I18n.t('sentOn'),
}
];
const newsletter_actions = [
{
name: 'view',
link: function(newsletter) {
return (
<a href={ newsletter.preview_url } target="_blank">
{MailPoet.I18n.t('preview')}
</a>
);
}
}
];
const NewsletterListNotificationHistory = React.createClass({
mixins: [QueueMixin, StatisticsMixin],
renderItem: function(newsletter, actions) {
const rowClasses = classNames(
'manage-column',
'column-primary',
'has-row-actions'
);
const segments = newsletter.segments.map(function(segment) {
return segment.name
}).join(', ');
return (
<div>
<td className={ rowClasses }>
<strong>
<a
href={ newsletter.preview_url }
target="_blank"
>{ newsletter.subject }</a>
</strong>
{ actions }
</td>
<td className="column" data-colname={ MailPoet.I18n.t('status') }>
{ this.renderQueueStatus(newsletter) }
</td>
<td className="column" data-colname={ MailPoet.I18n.t('lists') }>
{ segments }
</td>
{ (mailpoet_tracking_enabled === true) ? (
<td className="column" data-colname={ MailPoet.I18n.t('statistics') }>
{ this.renderStatistics(newsletter) }
</td>
) : null }
<td className="column-date" data-colname={ MailPoet.I18n.t('lastModifiedOn') }>
<abbr>{ MailPoet.Date.format(newsletter.updated_at) }</abbr>
</td>
</div>
);
},
render: function() {
return (
<div>
<h1 className="title">
{MailPoet.I18n.t('pageTitle')} <Link className="page-title-action" to="/new">{MailPoet.I18n.t('new')}</Link>
</h1>
<ListingTabs tab="notification" />
<Link
className="page-title-action"
to="/notification"
>{MailPoet.I18n.t('backToPostNotifications')}</Link>
<Listing
limit={ mailpoet_listing_per_page }
location={ this.props.location }
params={ this.props.params }
endpoint="newsletters"
type="notification_history"
base_url="notification/history/:parent_id"
onRenderItem={ this.renderItem }
columns={columns}
item_actions={ newsletter_actions }
auto_refresh={ true }
sort_by="updated_at"
sort_order="desc"
/>
</div>
);
}
});
module.exports = NewsletterListNotificationHistory;

View File

@ -7,6 +7,8 @@ import MailPoet from 'mailpoet'
import Listing from 'listing/listing.jsx'
import ListingTabs from 'newsletters/listings/tabs.jsx'
import { QueueMixin, StatisticsMixin } from 'newsletters/listings/mixins.jsx'
const mailpoet_tracking_enabled = (!!(window['mailpoet_tracking_enabled']));
const messages = {
@ -139,135 +141,7 @@ const newsletter_actions = [
];
const NewsletterListStandard = React.createClass({
pauseSending: function(newsletter) {
MailPoet.Ajax.post({
endpoint: 'sendingQueue',
action: 'pause',
data: newsletter.id
}).done(function() {
jQuery('#resume_'+newsletter.id).show();
jQuery('#pause_'+newsletter.id).hide();
});
},
resumeSending: function(newsletter) {
MailPoet.Ajax.post({
endpoint: 'sendingQueue',
action: 'resume',
data: newsletter.id
}).done(function() {
jQuery('#pause_'+newsletter.id).show();
jQuery('#resume_'+newsletter.id).hide();
});
},
renderStatus: function(newsletter) {
if (!newsletter.queue) {
return (
<span>{MailPoet.I18n.t('notSentYet')}</span>
);
} else {
if (newsletter.queue.status === 'scheduled') {
return (
<span>{MailPoet.I18n.t('scheduledFor')} { MailPoet.Date.format(newsletter.queue.scheduled_at) } </span>
)
}
const progressClasses = classNames(
'mailpoet_progress',
{ 'mailpoet_progress_complete': newsletter.queue.status === 'completed'}
);
// calculate percentage done
const percentage = Math.round(
(newsletter.queue.count_processed * 100) / (newsletter.queue.count_total)
);
let label;
if (newsletter.queue.status === 'completed') {
label = (
<span>
{
MailPoet.I18n.t('newsletterQueueCompleted')
.replace("%$1d", newsletter.queue.count_processed - newsletter.queue.count_failed)
.replace("%$2d", newsletter.queue.count_total)
}
</span>
);
} else {
label = (
<span>
{ newsletter.queue.count_processed } / { newsletter.queue.count_total }
&nbsp;&nbsp;
<a
id={ 'resume_'+newsletter.id }
className="button"
style={{ display: (newsletter.queue.status === 'paused') ? 'inline-block': 'none' }}
href="javascript:;"
onClick={ this.resumeSending.bind(null, newsletter) }
>{MailPoet.I18n.t('resume')}</a>
<a
id={ 'pause_'+newsletter.id }
className="button mailpoet_pause"
style={{ display: (newsletter.queue.status === null) ? 'inline-block': 'none' }}
href="javascript:;"
onClick={ this.pauseSending.bind(null, newsletter) }
>{MailPoet.I18n.t('pause')}</a>
</span>
);
}
return (
<div>
<div className={ progressClasses }>
<span
className="mailpoet_progress_bar"
style={ { width: percentage + "%"} }
></span>
<span className="mailpoet_progress_label">
{ percentage + "%" }
</span>
</div>
<p style={{ textAlign:'center' }}>
{ label }
</p>
</div>
);
}
},
renderStatistics: function(newsletter) {
if (mailpoet_tracking_enabled === false) {
return;
}
if (newsletter.statistics && newsletter.queue && newsletter.queue.status !== 'scheduled') {
const total_sent = ~~(newsletter.queue.count_processed);
let percentage_clicked = 0;
let percentage_opened = 0;
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
);
}
return (
<span>
{ percentage_opened }%, { percentage_clicked }%, { percentage_unsubscribed }%
</span>
);
} else {
return (
<span>{MailPoet.I18n.t('notSentYet')}</span>
);
}
},
mixins: [QueueMixin, StatisticsMixin],
renderItem: function(newsletter, actions) {
const rowClasses = classNames(
'manage-column',
@ -291,7 +165,7 @@ const NewsletterListStandard = React.createClass({
{ actions }
</td>
<td className="column" data-colname={ MailPoet.I18n.t('status') }>
{ this.renderStatus(newsletter) }
{ this.renderQueueStatus(newsletter) }
</td>
<td className="column" data-colname={ MailPoet.I18n.t('lists') }>
{ segments }
@ -321,7 +195,8 @@ const NewsletterListStandard = React.createClass({
location={ this.props.location }
params={ this.props.params }
endpoint="newsletters"
tab="standard"
type="standard"
base_url="standard"
onRenderItem={this.renderItem}
columns={columns}
bulk_actions={ bulk_actions }

View File

@ -343,7 +343,8 @@ const NewsletterListWelcome = React.createClass({
location={ this.props.location }
params={ this.props.params }
endpoint="newsletters"
tab="welcome"
type="welcome"
base_url="welcome"
onRenderItem={ this.renderItem }
columns={ columns }
bulk_actions={ bulk_actions }

View File

@ -14,12 +14,13 @@ import NewsletterTypeNotification from 'newsletters/types/notification/notificat
import NewsletterListStandard from 'newsletters/listings/standard.jsx'
import NewsletterListWelcome from 'newsletters/listings/welcome.jsx'
import NewsletterListNotification from 'newsletters/listings/notification.jsx'
import NewsletterListNotificationHistory from 'newsletters/listings/notification_history.jsx'
const history = useRouterHistory(createHashHistory)({ queryKey: false });
const App = React.createClass({
render() {
return this.props.children
return this.props.children;
}
});
@ -31,18 +32,16 @@ if(container) {
<Route path="/" component={ App }>
<IndexRedirect to="standard" />
{/* Listings */}
<Route name="listing/standard" path="standard" component={ NewsletterListStandard } />
<Route name="listing/welcome" path="welcome" component={ NewsletterListWelcome } />
<Route name="listing/notification" path="notification" component={ NewsletterListNotification } />
<Route path="standard/*" component={ NewsletterListStandard } />
<Route path="welcome/*" component={ NewsletterListWelcome } />
<Route path="notification/*" component={ NewsletterListNotification } />
<Route path="standard(/)**" params={{ tab: 'standard' }} component={ NewsletterListStandard } />
<Route path="welcome(/)**" component={ NewsletterListWelcome } />
<Route path="notification/history/:parent_id(/)**" component={ NewsletterListNotificationHistory } />
<Route path="notification(/)**" component={ NewsletterListNotification } />
{/* Newsletter: type selection */}
<Route path="new" component={ NewsletterTypes } />
{/* New newsletter: types */}
<Route name="new/standard" path="new/standard" component={ NewsletterTypeStandard } />
<Route name="new/welcome" path="new/welcome" component={ NewsletterTypeWelcome } />
<Route name="new/notification" path="new/notification" component={ NewsletterTypeNotification } />
<Route path="new/standard" component={ NewsletterTypeStandard } />
<Route path="new/welcome" component={ NewsletterTypeWelcome } />
<Route path="new/notification" component={ NewsletterTypeNotification } />
{/* Template selection */}
<Route name="template" path="template/:id" component={ NewsletterTemplates } />
{/* Sending options */}

View File

@ -32,7 +32,7 @@ class API {
$endpoint = self::ENDPOINT_NAMESPACE . ucfirst($this->endpoint);
if(!$this->api_request) return;
if(!$this->endpoint || !class_exists($endpoint)) {
$this->terminateRequest(self::RESPONSE_ERROR, __('Invalid API endpoint.'));
self::terminateRequest(self::RESPONSE_ERROR, __('Invalid API endpoint.'));
}
$this->callEndpoint(
$endpoint,
@ -43,7 +43,7 @@ class API {
function callEndpoint($endpoint, $action, $data) {
if(!method_exists($endpoint, $action)) {
$this->terminateRequest(self::RESPONSE_ERROR, __('Invalid API action.'));
self::terminateRequest(self::RESPONSE_ERROR, __('Invalid API action.'));
}
call_user_func(
array(
@ -83,7 +83,7 @@ class API {
return add_query_arg($params, home_url());
}
function terminateRequest($code, $message) {
static function terminateRequest($code, $message) {
status_header($code, $message);
exit;
}

View File

@ -56,6 +56,7 @@ class Initializer {
\ORM::configure(Env::$db_source_name);
\ORM::configure('username', Env::$db_username);
\ORM::configure('password', Env::$db_password);
\ORM::configure('logging', WP_DEBUG);
\ORM::configure('driver_options', array(
\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
\PDO::MYSQL_ATTR_INIT_COMMAND =>

View File

@ -173,6 +173,7 @@ class Migrator {
function newsletters() {
$attributes = array(
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'parent_id mediumint(9) NULL,',
'subject varchar(250) NOT NULL DEFAULT "",',
'type varchar(20) NOT NULL DEFAULT "standard",',
'sender_address varchar(150) NOT NULL DEFAULT "",',

View File

@ -67,29 +67,33 @@ class Scheduler {
}
function processPostNotificationNewsletter($newsletter, $queue) {
// ensure that segments exist
$segments = $newsletter->segments()->findArray();
if(empty($segments)) {
$this->deleteQueueOrUpdateNextRunDate($queue, $newsletter);
return;
}
$segment_ids = array_map(function($segment) {
return $segment['id'];
return (int)$segment['id'];
}, $segments);
// ensure that subscribers are in segments
$subscribers = Subscriber::getSubscribedInSegments($segment_ids)
->findArray();
$subscribers = Helpers::arrayColumn($subscribers, 'subscriber_id');
$subscribers = array_unique($subscribers);
if(empty($subscribers)) {
$this->deleteQueueOrUpdateNextRunDate($queue, $newsletter);
return;
}
// schedule new queue if the post notification is not destined for immediate delivery
if($newsletter->intervalType !== NewsletterScheduler::INTERVAL_IMMEDIATELY) {
$new_queue = SendingQueue::create();
$new_queue->newsletter_id = $newsletter->id;
$new_queue->status = SendingQueue::STATUS_SCHEDULED;
self::deleteQueueOrUpdateNextRunDate($new_queue, $newsletter);
}
// create a duplicate newsletter that acts as a history record
$notification_history = $this->createNotificationHistory($newsletter->id);
if(!$notification_history) return;
// queue newsletter for delivery
$queue->newsletter_id = $notification_history->id;
$queue->subscribers = serialize(
array(
'to_process' => $subscribers
@ -164,7 +168,7 @@ class Scheduler {
return true;
}
private function deleteQueueOrUpdateNextRunDate($queue, $newsletter) {
function deleteQueueOrUpdateNextRunDate($queue, $newsletter) {
if($newsletter->intervalType === NewsletterScheduler::INTERVAL_IMMEDIATELY) {
$queue->delete();
} else {
@ -173,4 +177,12 @@ class Scheduler {
$queue->save();
}
}
function createNotificationHistory($newsletter_id) {
$newsletter = Newsletter::findOne($newsletter_id);
$notification_history = $newsletter->createNotificationHistory();
return ($notification_history->getErrors() === false) ?
$notification_history :
false;
}
}

View File

@ -178,10 +178,11 @@ class SendingQueue {
if(!$queue->count_to_process) {
$queue->processed_at = current_time('mysql');
$queue->status = SendingQueueModel::STATUS_COMPLETED;
// set newsletter status to sent
// if it's a standard or post notificaiton newsletter, update its status to sent
$newsletter = NewsletterModel::findOne($queue->newsletter_id);
// if it's a standard newsletter, update its status
if($newsletter->type === NewsletterModel::TYPE_STANDARD) {
if($newsletter->type === NewsletterModel::TYPE_STANDARD ||
$newsletter->type === NewsletterModel::TYPE_NOTIFICATION_HISTORY
) {
$newsletter->setStatus(NewsletterModel::STATUS_SENT);
}
}

View File

@ -1,6 +1,7 @@
<?php
namespace MailPoet\Cron\Workers\SendingQueue\Tasks;
use MailPoet\Models\Newsletter;
use MailPoet\Models\NewsletterPost;
if(!defined('ABSPATH')) exit;
@ -18,9 +19,12 @@ class Posts {
if(!count($matched_posts_ids)) {
return $newsletter;
}
$newsletter_id = ($newsletter['type'] === Newsletter::TYPE_NOTIFICATION_HISTORY) ?
$newsletter['parent_id'] :
$newsletter['id'];
foreach($matched_posts_ids as $post_id) {
$newletter_post = NewsletterPost::create();
$newletter_post->newsletter_id = $newsletter['id'];
$newletter_post->newsletter_id = $newsletter_id;
$newletter_post->post_id = $post_id;
$newletter_post->save();
}

View File

@ -16,8 +16,8 @@ class Handler {
$this->model = \Model::factory($this->model_class);
$this->data = array(
// tabs
'tab' => (isset($data['tab']) ? $data['tab'] : false),
// extra parameters
'params' => (isset($data['params']) ? $data['params'] : array()),
// pagination
'offset' => (isset($data['offset']) ? (int)$data['offset'] : 0),
'limit' => (isset($data['limit'])
@ -121,7 +121,6 @@ class Handler {
$this->table_name.'.'.$this->data['sort_by']
)
->findMany();
} else {
$this->setFilter();
$this->setGroup();

View File

@ -10,6 +10,7 @@ class Newsletter extends Model {
const TYPE_STANDARD = 'standard';
const TYPE_WELCOME = 'welcome';
const TYPE_NOTIFICATION = 'notification';
const TYPE_NOTIFICATION_HISTORY = 'notification_history';
// standard newsletters
const STATUS_DRAFT = 'draft';
@ -19,7 +20,6 @@ class Newsletter extends Model {
// automatic newsletters status
const STATUS_ACTIVE = 'active';
function __construct() {
parent::__construct();
@ -56,7 +56,6 @@ class Newsletter extends Model {
}
function duplicate($data = array()) {
// get current newsletter's data as an array
$newsletter_data = $this->asArray();
// remove id so that it creates a new record
@ -109,6 +108,43 @@ class Newsletter extends Model {
return $duplicate;
}
function createNotificationHistory() {
$newsletter_data = $this->asArray();
// remove id so that it creates a new record
unset($newsletter_data['id']);
$data = array_merge(
$newsletter_data,
array(
'parent_id' => $this->id,
'type' => self::TYPE_NOTIFICATION_HISTORY,
'status' => self::STATUS_SENDING
)
);
$notification_history = self::create();
$notification_history->hydrate($data);
$notification_history->save();
if($notification_history->getErrors() === false) {
// create relationships between notification history and segments
$segments = $this->segments()->findArray();
if(!empty($segments)) {
foreach($segments as $segment) {
$relation = NewsletterSegment::create();
$relation->segment_id = $segment['id'];
$relation->newsletter_id = $notification_history->id;
$relation->save();
}
}
}
return $notification_history;
}
function asArray() {
$model = parent::asArray();
@ -125,6 +161,14 @@ class Newsletter extends Model {
return parent::delete();
}
function children() {
return $this->has_many(
__NAMESPACE__.'\Newsletter',
'parent_id',
'id'
);
}
function segments() {
return $this->has_many_through(
__NAMESPACE__.'\Segment',
@ -139,6 +183,11 @@ class Newsletter extends Model {
return $this;
}
function withChildrenCount() {
$this->children_count = $this->children()->count();
return $this;
}
function options() {
return $this->has_many_through(
__NAMESPACE__.'\NewsletterOptionField',
@ -189,15 +238,11 @@ class Newsletter extends Model {
} else {
$this->statistics = $statistics->asArray();
}
return $this;
}
function getStatistics() {
if($this->queue === false) {
return false;
}
return SendingQueue::tableAlias('queues')
$statistics_query = SendingQueue::tableAlias('queues')
->selectExpr(
'COUNT(DISTINCT(clicks.subscriber_id)) as clicked, ' .
'COUNT(DISTINCT(opens.subscriber_id)) as opened, ' .
@ -217,10 +262,23 @@ class Newsletter extends Model {
MP_STATISTICS_UNSUBSCRIBES_TABLE,
'queues.id = unsubscribes.queue_id',
'unsubscribes'
)
);
if($this->type === self::TYPE_WELCOME) {
return $statistics_query
->where('queues.newsletter_id', $this->id)
->where('queues.status', SendingQueue::STATUS_COMPLETED)
->findOne();
} else {
if($this->queue === false) {
return false;
} else {
return $statistics_query
->where('queues.id', $this->queue['id'])
->findOne();
}
}
}
static function search($orm, $search = '') {
if(strlen(trim($search)) > 0) {
@ -230,6 +288,15 @@ class Newsletter extends Model {
}
static function filters($data = array()) {
$type = isset($data['params']['type']) ? $data['params']['type'] : null;
// newsletter types without filters
if(in_array($type, array(
self::TYPE_NOTIFICATION_HISTORY
))) {
return false;
}
$segments = Segment::orderByAsc('name')->findMany();
$segment_list = array();
$segment_list[] = array(
@ -239,7 +306,7 @@ class Newsletter extends Model {
foreach($segments as $segment) {
$newsletters = $segment->newsletters()
->filter('filterType', $data['tab'])
->filter('filterType', $type)
->filter('groupBy', $data);
$newsletters_count = $newsletters->count();
@ -260,18 +327,32 @@ class Newsletter extends Model {
}
static function filterBy($orm, $data = array()) {
$type = isset($data['tab']) ? $data['tab'] : null;
// apply filters
if(!empty($data['filter'])) {
foreach($data['filter'] as $key => $value) {
if($key === 'segment') {
$segment = Segment::findOne($value);
if($segment !== false) {
$orm = $segment->newsletters()->filter('filterType', $type);
$orm = $segment->newsletters();
}
}
}
}
// filter by type
$type = isset($data['params']['type']) ? $data['params']['type'] : null;
if($type !== null) {
$orm->filter('filterType', $type);
}
// filter by parent id
$parent_id = isset($data['params']['parent_id'])
? (int)$data['params']['parent_id']
: null;
if($parent_id !== null) {
$orm->where('parent_id', $parent_id);
}
return $orm;
}
@ -306,7 +387,14 @@ class Newsletter extends Model {
}
static function groups($data = array()) {
$type = isset($data['tab']) ? $data['tab'] : null;
$type = isset($data['params']['type']) ? $data['params']['type'] : null;
// newsletter types without groups
if(in_array($type, array(
self::TYPE_NOTIFICATION_HISTORY
))) {
return false;
}
$groups = array(
array(
@ -431,7 +519,8 @@ class Newsletter extends Model {
if(in_array($type, array(
self::TYPE_STANDARD,
self::TYPE_WELCOME,
self::TYPE_NOTIFICATION
self::TYPE_NOTIFICATION,
self::TYPE_NOTIFICATION_HISTORY
))) {
$orm->where('type', $type);
}
@ -447,7 +536,6 @@ class Newsletter extends Model {
'updated_at',
'deleted_at'
))
->filter('filterType', $data['tab'])
->filter('filterBy', $data)
->filter('groupBy', $data)
->filter('search', $data['search']);

View File

@ -1,6 +1,7 @@
<?php
namespace MailPoet\Newsletter\Renderer\Blocks;
use MailPoet\Models\Newsletter;
use MailPoet\Newsletter\Renderer\StylesHelper;
class Renderer {
@ -10,7 +11,10 @@ class Renderer {
function __construct(array $newsletter, $posts = false) {
$this->newsletter = $newsletter;
$this->posts = array();
$this->ALC = new \MailPoet\Newsletter\AutomatedLatestContent($this->newsletter['id']);
$newsletter_id = ($newsletter['type'] === Newsletter::TYPE_NOTIFICATION_HISTORY) ?
$newsletter['parent_id'] :
$newsletter['id'];
$this->ALC = new \MailPoet\Newsletter\AutomatedLatestContent($newsletter_id);
}
function render($data, $column_count) {
@ -45,12 +49,12 @@ class Renderer {
function processAutomatedLatestContent($args, $column_count) {
$posts_to_exclude = $this->getPosts();
$ALCPosts = $this->ALC->getPosts($args, $posts_to_exclude);
foreach($ALCPosts as $post) {
$ALC_posts = $this->ALC->getPosts($args, $posts_to_exclude);
foreach($ALC_posts as $post) {
$posts_to_exclude[] = $post->ID;
}
$transformed_posts = array(
'blocks' => $this->ALC->transformPosts($args, $ALCPosts)
'blocks' => $this->ALC->transformPosts($args, $ALC_posts)
);
$this->setPosts($posts_to_exclude);
$transformed_posts = StylesHelper::applyTextAlignment($transformed_posts);

View File

@ -245,6 +245,7 @@ class Newsletters {
}
function listing($data = array()) {
$listing = new Listing\Handler(
'\MailPoet\Models\Newsletter',
$data
@ -267,6 +268,11 @@ class Newsletters {
$newsletter
->withOptions()
->withSegments()
->withChildrenCount();
} else if($newsletter->type === Newsletter::TYPE_NOTIFICATION_HISTORY) {
$newsletter
->withSegments()
->withSendingQueue()
->withStatistics();
}

View File

@ -37,8 +37,10 @@ class SendingQueue {
);
}
if($newsletter->type === Newsletter::TYPE_WELCOME) {
// set welcome email active
if($newsletter->type === Newsletter::TYPE_WELCOME ||
$newsletter->type === Newsletter::TYPE_NOTIFICATION
) {
// set newsletter status to active
$result = $newsletter->setStatus(Newsletter::STATUS_ACTIVE);
$errors = $result->getErrors();
@ -48,20 +50,16 @@ class SendingQueue {
'errors' => $errors
);
} else {
$message = ($newsletter->type === Newsletter::TYPE_WELCOME) ?
__('Your welcome email has been activated') :
__('Your post notification has been activated');
return array(
'result' => true,
'data' => array(
'message' => __('Your welcome email has been activated')
'message' => $message
)
);
}
} else if($newsletter->type === Newsletter::TYPE_NOTIFICATION) {
// Post Notifications
$newsletter = Scheduler::processPostNotificationSchedule($newsletter->id);
Scheduler::createPostNotificationQueue($newsletter);
// set post notification active
$newsletter->setStatus(Newsletter::STATUS_ACTIVE);
}
$queue = SendingQueueModel::whereNull('status')
@ -83,18 +81,6 @@ class SendingQueue {
$queue->newsletter_id = $newsletter->id;
}
if($newsletter->type === Newsletter::TYPE_NOTIFICATION) {
$queue->scheduled_at = Scheduler::getNextRunDate($newsletter->schedule);
$queue->status = SendingQueueModel::STATUS_SCHEDULED;
$queue->save();
return array(
'result' => true,
'data' => array(
'message' => __('Your post notification has been activated')
)
);
}
if((bool)$newsletter->isScheduled) {
// set newsletter status
$newsletter->setStatus(Newsletter::STATUS_SCHEDULED);

View File

@ -19,7 +19,9 @@ class NewsletterRendererTest extends MailPoetTest {
),
'id' => 1,
'subject' => 'Some subject',
'preheader' => 'Some preheader'
'preheader' => 'Some preheader',
'type' => 'standard',
'status' => 'active'
);
$this->renderer = new Renderer($this->newsletter);
$this->column_renderer = new ColumnRenderer();

View File

@ -229,6 +229,9 @@
'sendNthWeekDay': __('Send every %$1s %$2s of the month at %$3s'),
'sendImmediately': __('Send immediately'),
'ifNewContentToSegments': __("if there's new content to %$1s."),
'sendingToSegmentsNotSpecified': __('You need to select a segment to send to.')
'sendingToSegmentsNotSpecified': __('You need to select a segment to send to.'),
'backToPostNotifications': __('Back to Post notifications'),
'sentOn': __('Sent on')
}) %>
<% endblock %>