diff --git a/assets/css/src/admin.styl b/assets/css/src/admin.styl index 32bed150e9..cbc530bd5e 100644 --- a/assets/css/src/admin.styl +++ b/assets/css/src/admin.styl @@ -17,3 +17,5 @@ @require 'settings' @require 'progress_bar' + +@require 'subscribers' \ No newline at end of file diff --git a/assets/css/src/subscribers.styl b/assets/css/src/subscribers.styl new file mode 100644 index 0000000000..511f7f5a57 --- /dev/null +++ b/assets/css/src/subscribers.styl @@ -0,0 +1,3 @@ +#subscribers_container + .mailpoet_segments_unsubscribed + color: lighten(#555, 33) \ No newline at end of file diff --git a/assets/js/src/form/fields/selection.jsx b/assets/js/src/form/fields/selection.jsx index 8b50caf3c3..66fccbd56a 100644 --- a/assets/js/src/form/fields/selection.jsx +++ b/assets/js/src/form/fields/selection.jsx @@ -26,7 +26,7 @@ function( && (this.props.item.id !== prevProps.item.id) ) { jQuery('#'+this.refs.select.id) - .val(this.props.item[this.props.field.name]) + .val(this.getSelectedValues()) .trigger('change'); } }, @@ -45,7 +45,11 @@ function( if(item.element && item.element.selected) { return null; } else { - return item.text; + if(item.title) { + return item.title; + } else { + return item.text; + } } } }); @@ -65,15 +69,25 @@ function( select2.select2( 'val', - this.props.item[this.props.field.name] + this.getSelectedValues() ); this.setState({ initialized: true }); }, + getSelectedValues: function() { + if(this.props.field['selected'] !== undefined) { + return this.props.field['selected'](this.props.item); + } else if(this.props.item !== undefined && this.props.field.name !== undefined) { + return this.props.item[this.props.field.name]; + } else { + return null; + } + }, loadCachedItems: function() { if(typeof(window['mailpoet_'+this.props.field.endpoint]) !== 'undefined') { var items = window['mailpoet_'+this.props.field.endpoint]; + if(this.props.field['filter'] !== undefined) { items = items.filter(this.props.field.filter); } @@ -98,31 +112,48 @@ function( }); } }, + getLabel: function(item) { + if(this.props.field['getLabel'] !== undefined) { + return this.props.field.getLabel(item, this.props.item); + } + return item.name; + }, + getSearchLabel: function(item) { + if(this.props.field['getSearchLabel'] !== undefined) { + return this.props.field.getSearchLabel(item, this.props.item); + } + return null; + }, + getValue: function(item) { + if(this.props.field['getValue'] !== undefined) { + return this.props.field.getValue(item, this.props.item); + } + return item.id; + }, render: function() { - var options = this.state.items.map(function(item, index) { + const options = this.state.items.map((item, index) => { + let label = this.getLabel(item); + let searchLabel = this.getSearchLabel(item); + let value = this.getValue(item); + return ( ); }); - var default_value = ( - (this.props.item !== undefined && this.props.field.name !== undefined) - ? this.props.item[this.props.field.name] - : null - ); - return ( ); diff --git a/assets/js/src/segments/list.jsx b/assets/js/src/segments/list.jsx index 6574e62d8d..a98ebb0260 100644 --- a/assets/js/src/segments/list.jsx +++ b/assets/js/src/segments/list.jsx @@ -181,13 +181,13 @@ const SegmentList = React.createClass({ { segment.description } - { segment.subscribed || 0 } + { segment.subscribers_count.subscribed || 0 } - { segment.unconfirmed || 0 } + { segment.subscribers_count.unconfirmed || 0 } - { segment.unsubscribed || 0 } + { segment.subscribers_count.unsubscribed || 0 } { segment.created_at } diff --git a/assets/js/src/subscribers/form.jsx b/assets/js/src/subscribers/form.jsx index 203cad7923..94293250ae 100644 --- a/assets/js/src/subscribers/form.jsx +++ b/assets/js/src/subscribers/form.jsx @@ -3,15 +3,16 @@ define( 'react', 'react-router', 'mailpoet', - 'form/form.jsx' + 'form/form.jsx', + 'moment' ], function( React, Router, MailPoet, - Form + Form, + Moment ) { - var fields = [ { name: 'email', @@ -45,8 +46,38 @@ define( placeholder: "Select a list", endpoint: "segments", multiple: true, + selected: function(subscriber) { + if (Array.isArray(subscriber.subscriptions) === false) { + return null; + } + + return subscriber.subscriptions.map(function(subscription) { + if (subscription.status === 'subscribed') { + return subscription.segment_id; + } + }); + }, filter: function(segment) { return !!(!segment.deleted_at); + }, + getSearchLabel: function(segment, subscriber) { + let label = ''; + + if (subscriber.subscriptions !== undefined) { + subscriber.subscriptions.map(function(subscription) { + if (segment.id === subscription.segment_id) { + label = segment.name; + + if (subscription.status === 'unsubscribed') { + const unsubscribed_at = Moment(subscription.updated_at) + .utcOffset(parseInt(mailpoet_date_offset)) + .format('ddd, D MMM YYYY HH:mm:ss'); + label += ' (Unsubscribed on '+unsubscribed_at+')'; + } + } + }); + } + return label; } } ]; @@ -58,11 +89,11 @@ define( label: custom_field.name, type: custom_field.type }; - if(custom_field.params) { + if (custom_field.params) { field.params = custom_field.params; } - if(custom_field.params.values) { + if (custom_field.params.values) { field.values = custom_field.params.values; } diff --git a/assets/js/src/subscribers/list.jsx b/assets/js/src/subscribers/list.jsx index 9993b4fd0c..a6c5120e73 100644 --- a/assets/js/src/subscribers/list.jsx +++ b/assets/js/src/subscribers/list.jsx @@ -231,6 +231,15 @@ const item_actions = [ ]; const SubscriberList = React.createClass({ + getSegmentFromId: function(segment_id) { + let result = false; + mailpoet_segments.map(function(segment) { + if (segment.id === segment_id) { + result = segment; + } + }); + return result; + }, renderItem: function(subscriber, actions) { let row_classes = classNames( 'manage-column', @@ -255,11 +264,41 @@ const SubscriberList = React.createClass({ break; } - let segments = mailpoet_segments.filter(function(segment) { - return (jQuery.inArray(segment.id, subscriber.segments) !== -1); - }).map(function(segment) { - return segment.name; - }).join(', '); + let segments = false; + + if (subscriber.subscriptions.length > 0) { + let subscribed_segments = []; + let unsubscribed_segments = []; + + subscriber.subscriptions.map((subscription) => { + const segment = this.getSegmentFromId(subscription.segment_id); + if (subscription.status === 'subscribed') { + subscribed_segments.push(segment.name); + } else { + unsubscribed_segments.push(segment.name); + } + }); + + segments = ( + + + { subscribed_segments.join(', ') } + { + ( + subscribed_segments.length > 0 + && unsubscribed_segments.length > 0 + ) ? ' / ' : '' + } + + + { unsubscribed_segments.join(', ') } + + + ); + } let avatar = false; if(subscriber.avatar_url) { diff --git a/lib/Form/Util/Export.php b/lib/Form/Util/Export.php index 53edb33b0a..92c65d4a05 100644 --- a/lib/Form/Util/Export.php +++ b/lib/Form/Util/Export.php @@ -22,19 +22,21 @@ class Export { ), site_url()); // generate iframe - return ''; + return join(' ', array( + '' + )); break; case 'php': diff --git a/lib/Listing/Handler.php b/lib/Listing/Handler.php index 7656cbb491..ba10bb8438 100644 --- a/lib/Listing/Handler.php +++ b/lib/Listing/Handler.php @@ -89,7 +89,7 @@ class Handler { $items = $this->model ->offset($this->data['offset']) ->limit($this->data['limit']) - ->findArray(); + ->findMany(); return array( 'count' => $count, diff --git a/lib/Models/Newsletter.php b/lib/Models/Newsletter.php index 74273934d0..628019fbca 100644 --- a/lib/Models/Newsletter.php +++ b/lib/Models/Newsletter.php @@ -52,6 +52,11 @@ class Newsletter extends Model { ); } + function withSegments() { + $this->segments = $this->segments()->findArray(); + return $this; + } + function options() { return $this->has_many_through( __NAMESPACE__.'\NewsletterOptionField', @@ -67,6 +72,12 @@ class Newsletter extends Model { ->findOne(); } + function withSendingQueue() { + $this->queue = $this->getQueue(); + return $this; + } + + static function search($orm, $search = '') { return $orm->where_like('subject', '%' . $search . '%'); } diff --git a/lib/Models/Segment.php b/lib/Models/Segment.php index 41ef874e1a..6e14ad8df3 100644 --- a/lib/Models/Segment.php +++ b/lib/Models/Segment.php @@ -38,15 +38,6 @@ class Segment extends Model { ); } - function segmentFilters() { - return $this->has_many_through( - __NAMESPACE__.'\Filter', - __NAMESPACE__.'\SegmentFilter', - 'segment_id', - 'filter_id' - ); - } - function duplicate($data = array()) { $duplicate = parent::duplicate($data); @@ -76,6 +67,32 @@ class Segment extends Model { ->delete(); } + function withSubscribersCount() { + $this->subscribers_count = SubscriberSegment::table_alias('relation') + ->where('relation.segment_id', $this->id) + ->join( + MP_SUBSCRIBERS_TABLE, + 'subscribers.id = relation.subscriber_id', + 'subscribers' + ) + ->select_expr( + 'SUM(CASE subscribers.status WHEN "subscribed" THEN 1 ELSE 0 END)', + 'subscribed' + ) + ->select_expr( + 'SUM(CASE subscribers.status WHEN "unsubscribed" THEN 1 ELSE 0 END)', + 'unsubscribed' + ) + ->select_expr( + 'SUM(CASE subscribers.status WHEN "unconfirmed" THEN 1 ELSE 0 END)', + 'unconfirmed' + ) + ->findOne() + ->asArray(); + + return $this; + } + static function getWPUsers() { return self::where('type', 'wp_users')->findOne(); } diff --git a/lib/Models/Subscriber.php b/lib/Models/Subscriber.php index 82f5d0ca82..d9de919ffe 100644 --- a/lib/Models/Subscriber.php +++ b/lib/Models/Subscriber.php @@ -291,7 +291,7 @@ class Subscriber extends Model { } } if($segment_ids !== false) { - $subscriber->addToSegments($segment_ids); + SubscriberSegment::setSubscriptions($subscriber, $segment_ids); } } return $subscriber; @@ -314,6 +314,17 @@ class Subscriber extends Model { return $this; } + function withSegments() { + $this->segments = $this->segments()->findArray(); + return $this; + } + + function withSubscriptions() { + $this->subscriptions = SubscriberSegment::where('subscriber_id', $this->id()) + ->findArray(); + return $this; + } + function getCustomField($custom_field_id, $default = null) { $custom_field = SubscriberCustomField::select('value') ->where('custom_field_id', $custom_field_id) diff --git a/lib/Models/SubscriberSegment.php b/lib/Models/SubscriberSegment.php index c538c23602..90f3a6d419 100644 --- a/lib/Models/SubscriberSegment.php +++ b/lib/Models/SubscriberSegment.php @@ -12,6 +12,28 @@ class SubscriberSegment extends Model { parent::__construct(); } + static function setSubscriptions($subscriber, $segment_ids = array()) { + if($subscriber->id > 0) { + // unsubscribe from current subscriptions + SubscriberSegment::where('subscriber_id', $subscriber->id) + ->whereNotIn('segment_id', $segment_ids) + ->findResultSet() + ->set('status', 'unsubscribed') + ->save(); + + // subscribe to segments + foreach($segment_ids as $segment_id) { + self::createOrUpdate(array( + 'subscriber_id' => $subscriber->id, + 'segment_id' => $segment_id, + 'status' => 'subscribed' + )); + } + } + + return $subscriber; + } + static function filterWithCustomFields($orm) { $orm = $orm->select(MP_SUBSCRIBERS_TABLE.'.*'); $customFields = CustomField::findArray(); @@ -37,6 +59,30 @@ class SubscriberSegment extends Model { return $orm->where('status', 'subscribed'); } + static function createOrUpdate($data = array()) { + $subscription = false; + + if(isset($data['id']) && (int)$data['id'] > 0) { + $subscription = self::findOne((int)$data['id']); + } + + if(isset($data['subscriber_id']) && isset($data['segment_id'])) { + $subscription = self::where('subscriber_id', (int)$data['subscriber_id']) + ->where('segment_id', (int)$data['segment_id']) + ->findOne(); + } + + if($subscription === false) { + $subscription = self::create(); + $subscription->hydrate($data); + } else { + unset($data['id']); + $subscription->set($data); + } + + return $subscription->save(); + } + static function createMultiple($segmnets, $subscribers) { $values = Helpers::flattenArray( array_map(function ($segment) use ($subscribers) { diff --git a/lib/Router/Forms.php b/lib/Router/Forms.php index c47869864a..af73bdcee8 100644 --- a/lib/Router/Forms.php +++ b/lib/Router/Forms.php @@ -13,11 +13,10 @@ class Forms { function get($id = false) { $form = Form::findOne($id); - if($form === false) { - return false; - } else { - return $form->asArray(); + if($form !== false) { + $form = $form->asArray(); } + return $form; } function listing($data = array()) { @@ -29,19 +28,14 @@ class Forms { $listing_data = $listing->get(); // fetch segments relations for each returned item - foreach($listing_data['items'] as &$item) { - // form's segments - $form_settings = ( - (is_serialized($item['settings'])) - ? unserialize($item['settings']) - : array() - ); - - $item['segments'] = ( - !empty($form_settings['segments']) - ? $form_settings['segments'] + foreach($listing_data['items'] as $key => $form) { + $form = $form->asArray(); + $form['segments'] = ( + !empty($form['settings']['segments']) + ? $form['settings']['segments'] : array() ); + $listing_data['items'][$key] = $form; } return $listing_data; diff --git a/lib/Router/Newsletters.php b/lib/Router/Newsletters.php index 61084a444d..2f40669a52 100644 --- a/lib/Router/Newsletters.php +++ b/lib/Router/Newsletters.php @@ -206,20 +206,11 @@ class Newsletters { $listing_data = $listing->get(); - foreach($listing_data['items'] as &$item) { - // get segments - $segments = NewsletterSegment::select('segment_id') - ->where('newsletter_id', $item['id']) - ->findMany(); - $item['segments'] = array_map(function($relation) { - return $relation->segment_id; - }, $segments); - - // get queue - $queue = SendingQueue::where('newsletter_id', $item['id']) - ->orderByDesc('updated_at') - ->findOne(); - $item['queue'] = ($queue !== false) ? $queue->asArray() : null; + foreach($listing_data['items'] as $key => $newsletter) { + $listing_data['items'][$key] = $newsletter + ->withSegments() + ->withSendingQueue() + ->asArray(); } return $listing_data; diff --git a/lib/Router/Segments.php b/lib/Router/Segments.php index 687f6dde86..93ffdfb11a 100644 --- a/lib/Router/Segments.php +++ b/lib/Router/Segments.php @@ -30,36 +30,14 @@ class Segments { $listing_data = $listing->get(); // fetch segments relations for each returned item - foreach($listing_data['items'] as &$item) { - $stats = SubscriberSegment::table_alias('relation') - ->where( - 'relation.segment_id', - $item['id'] - ) - ->join( - MP_SUBSCRIBERS_TABLE, - 'subscribers.id = relation.subscriber_id', - 'subscribers' - ) - ->select_expr( - 'SUM(CASE subscribers.status WHEN "subscribed" THEN 1 ELSE 0 END)', - 'subscribed' - ) - ->select_expr( - 'SUM(CASE subscribers.status WHEN "unsubscribed" THEN 1 ELSE 0 END)', - 'unsubscribed' - ) - ->select_expr( - 'SUM(CASE subscribers.status WHEN "unconfirmed" THEN 1 ELSE 0 END)', - 'unconfirmed' - ) - ->findOne()->asArray(); - - $item = array_merge($item, $stats); - - $item['subscribers_url'] = admin_url( - 'admin.php?page=mailpoet-subscribers#/filter[segment='.$item['id'].']' + foreach($listing_data['items'] as $key => $segment) { + $segment->subscribers_url = admin_url( + 'admin.php?page=mailpoet-subscribers#/filter[segment='.$segment->id.']' ); + + $listing_data['items'][$key] = $segment + ->withSubscribersCount() + ->asArray(); } return $listing_data; diff --git a/lib/Router/Subscribers.php b/lib/Router/Subscribers.php index e3b8a2940f..a1aa12376d 100644 --- a/lib/Router/Subscribers.php +++ b/lib/Router/Subscribers.php @@ -17,15 +17,12 @@ class Subscribers { function get($id = false) { $subscriber = Subscriber::findOne($id); - if($subscriber !== false && $subscriber->id() > 0) { - $segments = $subscriber->segments()->findArray(); - - $subscriber = $subscriber->withCustomFields()->asArray(); - $subscriber['segments'] = array_map(function($segment) { - return $segment['id']; - }, $segments); + if($subscriber !== false) { + $subscriber = $subscriber + ->withCustomFields() + ->withSubscriptions() + ->asArray(); } - return $subscriber; } @@ -38,19 +35,10 @@ class Subscribers { $listing_data = $listing->get(); // fetch segments relations for each returned item - foreach($listing_data['items'] as &$item) { - // avatar - $item['avatar_url'] = get_avatar_url($item['email'], array( - 'size' => 32 - )); - - // subscriber's segments - $relations = SubscriberSegment::select('segment_id') - ->where('subscriber_id', $item['id']) - ->findMany(); - $item['segments'] = array_map(function($relation) { - return $relation->segment_id; - }, $relations); + foreach($listing_data['items'] as $key => $subscriber) { + $listing_data['items'][$key] = $subscriber + ->withSubscriptions() + ->asArray(); } return $listing_data; diff --git a/lib/Twig/i18n.php b/lib/Twig/i18n.php index 8ae1166bc0..b99f97a906 100644 --- a/lib/Twig/i18n.php +++ b/lib/Twig/i18n.php @@ -18,7 +18,13 @@ class i18n extends \Twig_Extension { // twig custom functions $twig_functions = array(); // list of WP functions to map - $functions = array('localize', '__', '_n'); + $functions = array( + 'localize', + '__', + '_n', + 'date', + 'date_format' + ); foreach($functions as $function) { $twig_functions[] = new \Twig_SimpleFunction( @@ -57,6 +63,23 @@ class i18n extends \Twig_Extension { return call_user_func_array('_n', $this->setTextDomain($args)); } + function date() { + $args = func_get_args(); + $date = (isset($args[0])) ? $args[0] : null; + $date_format = (isset($args[1])) ? $args[1] : get_option('date_format'); + + if(empty($date)) return; + + // check if it's an int passed as a string + if((string)(int)$date === $date) { + $date = (int)$date; + } else if(!is_int($date)) { + $date = strtotime($date); + } + + return get_date_from_gmt(date('Y-m-d H:i:s', $date), $date_format); + } + private function setTextDomain($args = array()) { // make sure that the last argument is our text domain if($args[count($args) - 1] !== $this->_text_domain) { diff --git a/tests/unit/Router/NewslettersCest.php b/tests/unit/Router/NewslettersCest.php index aa45164564..23b45c28e1 100644 --- a/tests/unit/Router/NewslettersCest.php +++ b/tests/unit/Router/NewslettersCest.php @@ -160,13 +160,18 @@ class NewslettersCest { expect($response['items'][0]['subject'])->equals('My Standard Newsletter'); expect($response['items'][1]['subject'])->equals('My Post Notification'); - expect($response['items'][0]['segments'])->equals(array( - $segment_1->id(), - $segment_2->id() - )); - expect($response['items'][1]['segments'])->equals(array( - $segment_2->id() - )); + + // 1st subscriber has 2 segments + expect($response['items'][0]['segments'])->count(2); + expect($response['items'][0]['segments'][0]['id']) + ->equals($segment_1->id); + expect($response['items'][0]['segments'][1]['id']) + ->equals($segment_2->id); + + // 2nd subscriber has 1 segment + expect($response['items'][1]['segments'])->count(1); + expect($response['items'][1]['segments'][0]['id']) + ->equals($segment_2->id); } function itCanBulkDeleteNewsletters() { @@ -193,6 +198,7 @@ class NewslettersCest { function _after() { Newsletter::deleteMany(); + NewsletterSegment::deleteMany(); Segment::deleteMany(); } } \ No newline at end of file diff --git a/views/subscribers/subscribers.html b/views/subscribers/subscribers.html index 143ddbf2ad..3f668cd26e 100644 --- a/views/subscribers/subscribers.html +++ b/views/subscribers/subscribers.html @@ -20,5 +20,7 @@ var mailpoet_segments = <%= json_encode(segments) %>; var mailpoet_custom_fields = <%= json_encode(custom_fields) %>; var mailpoet_month_names = <%= json_encode(month_names) %>; + var mailpoet_date_format = "<%= get_option('date_format') %>"; + var mailpoet_date_offset = "<%= get_option('gmt_offset') %>"; <% endblock %>