- Finishes import migration
- Updates models - Improves Notice.js
This commit is contained in:
@ -145,7 +145,7 @@ define(
|
||||
mailChimpListsContainerElement.hide();
|
||||
jQuery('.mailpoet_mailchimp-key-status').html('').removeClass('mailpoet_mailchimp-ok mailpoet_mailchimp-error');
|
||||
mailChimpKeyVerifyButtonEelement.prop('disabled', true);
|
||||
toggleNextStepButton(mailChimpProcessButtonElement, 'on');
|
||||
toggleNextStepButton(mailChimpProcessButtonElement, 'off');
|
||||
}
|
||||
else {
|
||||
mailChimpKeyVerifyButtonEelement.prop('disabled', false);
|
||||
@ -158,21 +158,21 @@ define(
|
||||
endpoint: 'import',
|
||||
action: 'getMailChimpLists',
|
||||
data: {api_key: mailChimpKeyInputElement.val()}
|
||||
}).done(function (result) {
|
||||
if (result.status !== 'success') {
|
||||
}).done(function (request) {
|
||||
if (request.result === false) {
|
||||
MailPoet.Notice.hide();
|
||||
MailPoet.Notice.error(result.message);
|
||||
MailPoet.Notice.error(request.message);
|
||||
jQuery('.mailpoet_mailchimp-key-status').removeClass().addClass('mailpoet_mailchimp-key-status mailpoet_mailchimp-error');
|
||||
mailChimpListsContainerElement.hide();
|
||||
toggleNextStepButton(mailChimpProcessButtonElement, 'off');
|
||||
} else {
|
||||
jQuery('.mailpoet_mailchimp-key-status').html('').removeClass().addClass('mailpoet_mailchimp-key-status mailpoet_mailchimp-ok');
|
||||
if (!result.data) {
|
||||
if (!request.data) {
|
||||
jQuery('.mailpoet_mailchimp-key-status').html(MailPoetI18n.noMailChimpLists);
|
||||
mailChimpListsContainerElement.hide();
|
||||
toggleNextStepButton(mailChimpProcessButtonElement, 'off');
|
||||
} else {
|
||||
displayMailChimpLists(result.data);
|
||||
displayMailChimpLists(request.data);
|
||||
}
|
||||
}
|
||||
MailPoet.Modal.loading(false);
|
||||
@ -195,17 +195,17 @@ define(
|
||||
api_key: mailChimpKeyInputElement.val(),
|
||||
lists: mailChimpListsContainerElement.find('select').val()
|
||||
}
|
||||
}).done(function (result) {
|
||||
if (result.status === 'success') {
|
||||
importData.step1 = result;
|
||||
}).done(function (request) {
|
||||
if (request.result === true) {
|
||||
importData.step1 = request.data;
|
||||
router.navigate('step2', {trigger: true});
|
||||
}
|
||||
else {
|
||||
MailPoet.Notice.hide();
|
||||
MailPoet.Notice.error(result.message);
|
||||
MailPoet.Notice.error(request.message);
|
||||
}
|
||||
MailPoet.Modal.loading(false);
|
||||
}).error(function (error) {
|
||||
}).error(function () {
|
||||
MailPoet.Modal.loading(false);
|
||||
MailPoet.Notice.error(MailPoetI18n.serverError + result.statusText.toLowerCase() + '.');
|
||||
});
|
||||
@ -376,7 +376,7 @@ define(
|
||||
return;
|
||||
}
|
||||
// define reusable variables
|
||||
var nextStepButton = jQuery('#step_2_process'),
|
||||
var nextStepButton = jQuery('#step2_process'),
|
||||
subscribers = jQuery.extend(true, {}, importData.step1), // create a copy of subscribers object for further manipulation
|
||||
subscribersDataTemplate = Handlebars.compile(jQuery('#subscribers_data_template').html()),
|
||||
subscribersDataTemplatePartial = Handlebars.compile(jQuery('#subscribers_data_template_partial').html()),
|
||||
@ -458,7 +458,7 @@ define(
|
||||
MailPoet.Notice.error(MailPoetI18n.segmentSelectionRequired, {
|
||||
static: true,
|
||||
scroll: true,
|
||||
addCustomClass: 'segmentSelection',
|
||||
id: 'segmentSelection',
|
||||
hideClose: true
|
||||
});
|
||||
}
|
||||
@ -510,18 +510,18 @@ define(
|
||||
description: segmentDescription
|
||||
}
|
||||
})
|
||||
.done(function (result) {
|
||||
if (result.status === 'success') {
|
||||
.done(function (request) {
|
||||
if (request.result === true) {
|
||||
mailpoetLists.push({
|
||||
'id': result.segment.id,
|
||||
'name': result.segment.name
|
||||
'id': request.segment.id,
|
||||
'name': request.segment.name
|
||||
});
|
||||
|
||||
var selected_values = segmentSelectElement.val();
|
||||
if (selected_values === null) {
|
||||
selected_values = [result.segment.id]
|
||||
selected_values = [request.segment.id]
|
||||
} else {
|
||||
selected_values.push(result.segment.id);
|
||||
selected_values.push(request.segment.id);
|
||||
}
|
||||
|
||||
enableListSelection(mailpoetLists);
|
||||
@ -532,7 +532,7 @@ define(
|
||||
}
|
||||
else {
|
||||
MailPoet.Modal.close();
|
||||
MailPoet.Notice.error(MailPoetI18n.segmentCreateError + result.message + '.');
|
||||
MailPoet.Notice.error(MailPoetI18n.segmentCreateError + request.message + '.');
|
||||
}
|
||||
})
|
||||
.error(function (error) {
|
||||
@ -559,7 +559,7 @@ define(
|
||||
column_id = 'ignore'; // set default column type
|
||||
// if the column is not undefined and has a valid e-mail, set type as email
|
||||
if (column_data % 1 !== 0 && emailRegex.test(column_data)) {
|
||||
column_id = 'subscriber_email';
|
||||
column_id = 's_email';
|
||||
} else if (subscribers.header) {
|
||||
var header_name = subscribers.header[i],
|
||||
header_name_match = mailpoet_columns.map(function (el) {
|
||||
@ -571,15 +571,15 @@ define(
|
||||
}// set column type using header name
|
||||
else if (header_name) {
|
||||
if (/first|first name|given name/i.test(header_name)) {
|
||||
column_id = 'subscriber_firstname';
|
||||
column_id = 's_first_name';
|
||||
} else if (/last|last name/i.test(header_name)) {
|
||||
column_id = 'subscriber_lastname';
|
||||
column_id = 's_last_name';
|
||||
} else if (/status/i.test(header_name)) {
|
||||
column_id = 'subscriber_state';
|
||||
column_id = 's_status';
|
||||
} else if (/subscribed|subscription/i.test(header_name)) {
|
||||
column_id = 'subscriber_confirmed_at';
|
||||
column_id = 's_confirmed_at';
|
||||
} else if (/ip/i.test(header_name)) {
|
||||
column_id = 'subscriber_confirmed_ip';
|
||||
column_id = 's_confirmed_ip';
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -681,10 +681,10 @@ define(
|
||||
type: type
|
||||
}
|
||||
})
|
||||
.done(function (result) {
|
||||
if (result.status === 'success') {
|
||||
.done(function (request) {
|
||||
if (request.result === true) {
|
||||
var new_column_data = {
|
||||
'id': result.customField.id,
|
||||
'id': request.customField.id,
|
||||
'name': name,
|
||||
'type': type,
|
||||
'custom': true,
|
||||
@ -759,7 +759,7 @@ define(
|
||||
|
||||
// filter subscribers' data to detect dates, emails, etc.
|
||||
function filterSubscribers() {
|
||||
jQuery('.mailpoet_invalidEmail, .mailpoet_invalidDate').remove();
|
||||
jQuery('[data-id="notice_invalidEmail"], [data-id="notice_invalidDate"]').remove();
|
||||
|
||||
var subscribersClone = jQuery.extend(true, {}, subscribers),
|
||||
preventNextStep = false,
|
||||
@ -775,15 +775,15 @@ define(
|
||||
var matchedColumn = jQuery.inArray(column.id, displayedColumnsIds);
|
||||
|
||||
// EMAIL filter: if the last value in the column doesn't have a valid email, hide the next button
|
||||
if (column.id === "subscriber_email") {
|
||||
if (column.id === "s_email") {
|
||||
if (!emailRegex.test(subscribersClone.subscribers[0][matchedColumn])) {
|
||||
preventNextStep = true;
|
||||
if (!jQuery('.mailpoet_invalidEmail').length) {
|
||||
if (!jQuery('[data-id="notice_invalidEmail"]').length) {
|
||||
MailPoet.Notice.error(MailPoetI18n.columnContainsInvalidElement, {
|
||||
static: true,
|
||||
scroll: true,
|
||||
hideClose: true,
|
||||
addCustomClass: 'invalidEmail'
|
||||
id: 'invalidEmail'
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -839,7 +839,7 @@ define(
|
||||
static: true,
|
||||
scroll: true,
|
||||
hideClose: true,
|
||||
addCustomClass: 'invalidDate'
|
||||
id: 'invalidDate'
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -899,23 +899,62 @@ define(
|
||||
segments: segmentSelectElement.val(),
|
||||
updateSubscribers: (jQuery(':radio[name="subscriber_update_option"]:checked').val() === 'yes') ? true : false
|
||||
})
|
||||
}).done(function (result) {
|
||||
if (result.status !== 'success') {
|
||||
MailPoet.Notice.error(result.message);
|
||||
}).done(function (request) {
|
||||
MailPoet.Modal.loading(false);
|
||||
if (request.result === false) {
|
||||
MailPoet.Notice.error(request.error);
|
||||
} else {
|
||||
alert('Processed ' + result.count)
|
||||
request.data.lists = [];
|
||||
importData.step2 = request.data;
|
||||
router.navigate('step3', {trigger: true});
|
||||
}
|
||||
}).error(function (error) {
|
||||
MailPoet.Modal.loading(false);
|
||||
MailPoet.Notice.error(MailPoetI18n.serverError + error.statusText.toLowerCase() + '.');
|
||||
});
|
||||
MailPoet.Modal.loading(false);
|
||||
});
|
||||
|
||||
filterSubscribers();
|
||||
enableListSelection(mailpoetLists);
|
||||
|
||||
});
|
||||
|
||||
router.on('route:step3', function () {
|
||||
if (typeof (importData.step2) === 'undefined') {
|
||||
router.navigate('step2', {trigger: true});
|
||||
return;
|
||||
}
|
||||
|
||||
showCurrentStep();
|
||||
|
||||
// display statistics
|
||||
var subscribers_data_import_results_template = Handlebars.compile(jQuery('#subscribers_data_import_results_template').html()),
|
||||
import_results = {
|
||||
added: (importData.step2.added) ? MailPoetI18n.subscribersAdded.replace('%1$s', '<strong>' + importData.step2.added + '</strong>').replace('%2$s', '"' + importData.step2.lists.join('", "') + '"') : false,
|
||||
updated: (importData.step2.updated) ? MailPoetI18n.subscribersUpdated.replace('%1$s', '<strong>' + importData.step2.updated + '</strong>').replace('%2$s', '"' + importData.step2.lists.join('", "') + '"') : false,
|
||||
noaction: (!importData.step2.updated && !importData.step2.added) ? true : false
|
||||
},
|
||||
export_menu_item = jQuery('span.mailpoet_export');
|
||||
|
||||
jQuery('#subscribers_data_import_results').html(subscribers_data_import_results_template(import_results)).show();
|
||||
|
||||
jQuery('a.mailpoet_import_again').off().click(function () {
|
||||
jQuery("#subscribers_data_import_results").hide();
|
||||
router.navigate('step1', {trigger: true});
|
||||
});
|
||||
|
||||
jQuery('a.mailpoet_view_subscribers').off().click(function () {
|
||||
window.location.href = 'admin.php?page=mailpoet-subscribers';
|
||||
});
|
||||
|
||||
// if new subscribers were added and the export menu item is hidden (it's shown only when there are subscribers), display it
|
||||
if (import_results.added && export_menu_item.not(':visible')) {
|
||||
export_menu_item.show();
|
||||
}
|
||||
|
||||
// reset previous step's data so that coming back to this step is prevented
|
||||
importData.step2 = undefined;
|
||||
});
|
||||
|
||||
if (!Backbone.History.started) {
|
||||
Backbone.history.start();
|
||||
|
@ -48,7 +48,7 @@ define('notice', ['mailpoet', 'jquery'], function(MailPoet, jQuery) {
|
||||
message: '',
|
||||
static: false,
|
||||
hideClose: false,
|
||||
addCustomClass: false,
|
||||
id: null,
|
||||
scroll: false,
|
||||
timeout: 2000,
|
||||
onOpen: null,
|
||||
@ -62,8 +62,8 @@ define('notice', ['mailpoet', 'jquery'], function(MailPoet, jQuery) {
|
||||
// clone element
|
||||
this.element = jQuery('#mailpoet_notice_'+this.options.type).clone();
|
||||
|
||||
// add custom identifier class to the element
|
||||
if (this.options.addCustomClass) this.element.addClass('mailpoet_'+this.options.addCustomClass);
|
||||
// add data-id to the element
|
||||
if (this.options.id) this.element.attr('data-id', 'notice_' + this.options.id);
|
||||
|
||||
// remove id from clone
|
||||
this.element.removeAttr('id');
|
||||
@ -168,12 +168,12 @@ define('notice', ['mailpoet', 'jquery'], function(MailPoet, jQuery) {
|
||||
if(all !== undefined && all === true) {
|
||||
jQuery('.mailpoet_notice:not([id])').trigger('close');
|
||||
} else if (all !== undefined && jQuery.isArray(all)) {
|
||||
for (var noticeClass in all) {
|
||||
jQuery('.mailpoet_'+all[noticeClass])
|
||||
for (var id in all) {
|
||||
jQuery('[data-id="notice_' + all[id] + '"]')
|
||||
.trigger('close');
|
||||
}
|
||||
} if (all !== undefined) {
|
||||
jQuery('.mailpoet_'+noticeClass)
|
||||
jQuery('[data-id="notice_' + all + '"]')
|
||||
.trigger('close');
|
||||
} else {
|
||||
jQuery('.mailpoet_notice.updated:not([id]), .mailpoet_notice.error:not([id])')
|
||||
|
@ -17,6 +17,8 @@ class Env {
|
||||
public static $db_prefix;
|
||||
public static $db_source_name;
|
||||
public static $db_host;
|
||||
public static $db_socket;
|
||||
public static $db_port;
|
||||
public static $db_name;
|
||||
public static $db_username;
|
||||
public static $db_password;
|
||||
@ -35,22 +37,33 @@ class Env {
|
||||
self::$lib_path = self::$path . '/lib';
|
||||
self::$plugin_prefix = 'mailpoet_';
|
||||
self::$db_prefix = $wpdb->prefix . self::$plugin_prefix;
|
||||
self::$db_source_name = self::dbSourceName();
|
||||
self::$db_host = DB_HOST;
|
||||
self::$db_port = 3306;
|
||||
self::$db_socket = false;
|
||||
if (preg_match('/(?=:\d+$)/', DB_HOST)) {
|
||||
list(self::$db_host, self::$db_port) = explode(':', DB_HOST);
|
||||
}
|
||||
else if (preg_match('/:/', DB_HOST)) {
|
||||
self::$db_socket = true;
|
||||
}
|
||||
self::$db_name = DB_NAME;
|
||||
self::$db_username = DB_USER;
|
||||
self::$db_password = DB_PASSWORD;
|
||||
self::$db_charset = $wpdb->get_charset_collate();
|
||||
self::$db_source_name = self::dbSourceName(self::$db_host, self::$db_socket, self::$db_port);
|
||||
}
|
||||
|
||||
private static function dbSourceName() {
|
||||
private static function dbSourceName($host, $socket,$port) {
|
||||
$source_name = array(
|
||||
'mysql:host=',
|
||||
DB_HOST,
|
||||
(!$socket) ? 'mysql:host=' : 'mysql:unix_socket=',
|
||||
$host,
|
||||
';',
|
||||
'port=',
|
||||
$port,
|
||||
';',
|
||||
'dbname=',
|
||||
DB_NAME
|
||||
);
|
||||
return implode('', $source_name);
|
||||
}
|
||||
}
|
||||
}
|
@ -166,7 +166,8 @@ class Migrator {
|
||||
'value varchar(255) NOT NULL,',
|
||||
'created_at TIMESTAMP NOT NULL DEFAULT 0,',
|
||||
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
|
||||
'PRIMARY KEY (id)'
|
||||
'PRIMARY KEY (id),',
|
||||
'UNIQUE KEY subscriber_id_custom_field_id (subscriber_id,custom_field_id)'
|
||||
);
|
||||
return $this->sqlify(__FUNCTION__, $attributes);
|
||||
}
|
||||
@ -192,7 +193,8 @@ class Migrator {
|
||||
'value varchar(255) NOT NULL,',
|
||||
'created_at TIMESTAMP NOT NULL DEFAULT 0,',
|
||||
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
|
||||
'PRIMARY KEY (id)'
|
||||
'PRIMARY KEY (id),',
|
||||
'UNIQUE KEY newsletter_id_option_field_id (newsletter_id,option_field_id)'
|
||||
);
|
||||
return $this->sqlify(__FUNCTION__, $attributes);
|
||||
}
|
||||
|
@ -5,43 +5,43 @@ use MailPoet\Models\Segment;
|
||||
use MailPoet\Util\Helpers;
|
||||
|
||||
class BootstrapMenu {
|
||||
|
||||
|
||||
function __construct() {
|
||||
$this->subscriberFields = $this->getSubscriberFields();
|
||||
$this->subscriberCustomFields = $this->getSubscriberCustomFields();
|
||||
$this->segments = $this->getSegments();
|
||||
}
|
||||
|
||||
|
||||
function getSubscriberFields() {
|
||||
return array(
|
||||
'subscriber_email' => __("Email"),
|
||||
'subscriber_firstname' => __("First name"),
|
||||
'subscriber_lastname' => __("Last name"),
|
||||
/* 'subscriber_confirmed_ip' => __("IP address"),
|
||||
'subscriber_confirmed_at' => __("Subscription date"),*/
|
||||
'subscriber_state' => __("Status")
|
||||
's_email' => __('Email'),
|
||||
's_first_name' => __('First name'),
|
||||
's_last_name' => __('Last name'),
|
||||
/* 's_confirmed_ip' => __('IP address'),
|
||||
's_confirmed_at' => __('Subscription date'),*/
|
||||
's_status' => __('Status')
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function getSegments() {
|
||||
return Segment::findArray();
|
||||
}
|
||||
|
||||
|
||||
function getSubscriberCustomFields() {
|
||||
return CustomField::findArray();
|
||||
}
|
||||
|
||||
|
||||
function formatSubscriberFields() {
|
||||
return array_map(function ($fieldId, $fieldName) {
|
||||
return array(
|
||||
'id' => $fieldId,
|
||||
'name' => $fieldName,
|
||||
'type' => ($fieldId === 'subscriber_confirmed_at') ? 'date' : null,
|
||||
'type' => ($fieldId === 's_confirmed_at') ? 'date' : null,
|
||||
'custom' => false
|
||||
);
|
||||
}, array_keys($this->subscriberFields), $this->subscriberFields);
|
||||
}
|
||||
|
||||
|
||||
function formatSubscriberCustomFields() {
|
||||
return array_map(function ($field) {
|
||||
return array(
|
||||
@ -53,36 +53,36 @@ class BootstrapMenu {
|
||||
);
|
||||
}, $this->subscriberCustomFields);
|
||||
}
|
||||
|
||||
|
||||
function formatSubscriberFieldsSelect2() {
|
||||
$select2Fields = array(
|
||||
array(
|
||||
'name' => __("Actions"),
|
||||
'name' => __('Actions'),
|
||||
'children' => array(
|
||||
array(
|
||||
'id' => 'ignore',
|
||||
'name' => __("Ignore column..."),
|
||||
'name' => __('Ignore column...'),
|
||||
),
|
||||
array(
|
||||
'id' => 'create',
|
||||
'name' => __("Create new column...")
|
||||
'name' => __('Create new column...')
|
||||
),
|
||||
)
|
||||
),
|
||||
array(
|
||||
'name' => __("System columns"),
|
||||
'name' => __('System columns'),
|
||||
'children' => $this->formatSubscriberFields()
|
||||
)
|
||||
);
|
||||
if($this->subscriberCustomFields) {
|
||||
array_push($select2Fields, array(
|
||||
'name' => __("User columns"),
|
||||
'name' => __('User columns'),
|
||||
'children' => $this->formatSubscriberCustomFields()
|
||||
));
|
||||
));
|
||||
}
|
||||
return $select2Fields;
|
||||
}
|
||||
|
||||
|
||||
function bootstrap() {
|
||||
$data['segments'] = array_map(function ($segment) {
|
||||
return array(
|
||||
@ -90,14 +90,14 @@ class BootstrapMenu {
|
||||
'name' => $segment['name'],
|
||||
);
|
||||
}, $this->getSegments());
|
||||
|
||||
|
||||
$data['subscriberFields'] = array_merge(
|
||||
$this->formatSubscriberFields(),
|
||||
$this->formatSubscriberCustomFields()
|
||||
);
|
||||
|
||||
|
||||
$data['subscriberFieldsSelect2'] = $this->formatSubscriberFieldsSelect2();
|
||||
|
||||
|
||||
$data = array_map('json_encode', $data);
|
||||
$data['maxPostSizeBytes'] = Helpers::getMaxPostSize('bytes');
|
||||
$data['maxPostSize'] = Helpers::getMaxPostSize();
|
||||
|
@ -1,5 +1,9 @@
|
||||
<?php namespace MailPoet\Import;
|
||||
|
||||
use MailPoet\Models\Subscriber;
|
||||
use MailPoet\Models\SubscriberCustomField;
|
||||
use MailPoet\Util\Helpers;
|
||||
|
||||
class Import {
|
||||
public function __construct($data) {
|
||||
$this->subscribersData = $data['subscribers'];
|
||||
@ -7,36 +11,136 @@ class Import {
|
||||
$this->updateSubscribers = $data['updateSubscribers'];
|
||||
$this->subscriberFields = $this->getSubscriberFields();
|
||||
$this->subscriberCustomFields = $this->getCustomSubscriberFields();
|
||||
$this->currentTime = time();
|
||||
$this->subscribersCount = count(reset($this->subscribersData));
|
||||
$this->currentTime = date('Y-m-d H:i:s');
|
||||
$this->profilerStart = microtime(true);
|
||||
}
|
||||
|
||||
function process() {
|
||||
// :)
|
||||
return array(
|
||||
'status' => 'success',
|
||||
'count' => count($this->subscribersData['subscriber_email'])
|
||||
$subscriberFields = $this->subscriberFields;
|
||||
$subscribersData = $this->subscribersData;
|
||||
$subscribersData = $this->filterSubscriberState($subscribersData);
|
||||
list($subscribersData, $subscriberFields) = $this->extendSubscribersAndFields(
|
||||
$subscribersData, $subscriberFields
|
||||
);
|
||||
if(in_array('subscriber_status', $subscriberFields)) {
|
||||
$this->subscribersData['subscriber_state'] = $this->filterSubscriberState(
|
||||
$this->subscribersData['subscriber_state']
|
||||
list($existingSubscribers, $newSubscribers) = $this->splitSubscribers(
|
||||
$subscribersData
|
||||
);
|
||||
$addedSubscribers = $updatedSubscribers = array();
|
||||
if($newSubscribers) {
|
||||
$addedSubscribers = $this->addOrUpdateSubscribers(
|
||||
'create',
|
||||
$newSubscribers,
|
||||
$subscriberFields
|
||||
);
|
||||
|
||||
}
|
||||
if($existingSubscribers && $this->updateSubscribers) {
|
||||
$updatedSubscribers = $this->addOrUpdateSubscribers(
|
||||
'update',
|
||||
$existingSubscribers,
|
||||
$subscriberFields
|
||||
);
|
||||
if($addedSubscribers) {
|
||||
$updatedSubscribers = array_diff_key(
|
||||
$updatedSubscribers,
|
||||
$addedSubscribers
|
||||
);
|
||||
}
|
||||
}
|
||||
return array(
|
||||
'result' => true,
|
||||
'data' => array(
|
||||
'added' => count($addedSubscribers),
|
||||
'updated' => count($updatedSubscribers),
|
||||
),
|
||||
'profile' => $this->timeExecution()
|
||||
);
|
||||
}
|
||||
|
||||
function splitSubscribers($subscribersData) {
|
||||
$existingRecords = array_filter(
|
||||
array_map(function ($subscriberEmails) {
|
||||
return Subscriber::selectMany(array('email'))
|
||||
->whereIn('email', $subscriberEmails)
|
||||
->findArray();
|
||||
}, array_chunk($subscribersData['s_email'], 200))
|
||||
);
|
||||
if(!$existingRecords) {
|
||||
return array(
|
||||
false,
|
||||
$subscribersData
|
||||
);
|
||||
}
|
||||
$existingRecords = Helpers::flattenArray($existingRecords);
|
||||
$newRecords = array_keys(
|
||||
array_diff(
|
||||
$subscribersData['s_email'],
|
||||
$existingRecords
|
||||
)
|
||||
);
|
||||
if(!$newRecords) {
|
||||
return array(
|
||||
$subscribersData,
|
||||
false
|
||||
);
|
||||
}
|
||||
$newSubscribers =
|
||||
array_filter(
|
||||
array_map(function ($subscriber) use ($newRecords) {
|
||||
return array_map(function ($index) use ($subscriber) {
|
||||
return $subscriber[$index];
|
||||
}, $newRecords);
|
||||
}, $subscribersData)
|
||||
);
|
||||
|
||||
$existingSubscribers =
|
||||
array_map(function ($subscriber) use ($newRecords) {
|
||||
return array_values( // reindex array
|
||||
array_filter( // remove NULL entries
|
||||
array_map(function ($index, $data) use ($newRecords) {
|
||||
if(!in_array($index, $newRecords)) return $data;
|
||||
}, array_keys($subscriber), $subscriber)
|
||||
)
|
||||
);
|
||||
}, $subscribersData);
|
||||
return array(
|
||||
$existingSubscribers,
|
||||
$newSubscribers
|
||||
);
|
||||
}
|
||||
|
||||
function extendSubscribersAndFields($subscribersData, $subscriberFields) {
|
||||
$subscribersData['created_at'] = $this->filterSubscriberCreatedAtDate();
|
||||
$subscriberFields[] = 'created_at';
|
||||
return array(
|
||||
$subscribersData,
|
||||
$subscriberFields
|
||||
);
|
||||
}
|
||||
|
||||
function getSubscriberFields() {
|
||||
return array_map(function ($field) {
|
||||
if(!is_int($field)) return $field;
|
||||
}, array_keys($this->subscribersData));
|
||||
return array_filter(
|
||||
array_map(function ($field) {
|
||||
if(!is_int($field)) return $field;
|
||||
}, array_keys($this->subscribersData))
|
||||
);
|
||||
}
|
||||
|
||||
function getCustomSubscriberFields() {
|
||||
return array_map(function ($field) {
|
||||
if(is_int($field)) return $field;
|
||||
}, array_keys($this->subscribersData));
|
||||
return array_filter(
|
||||
array_map(function ($field) {
|
||||
if(is_int($field)) return $field;
|
||||
}, array_keys($this->subscribersData))
|
||||
);
|
||||
}
|
||||
|
||||
function filterSubscriberState($data) {
|
||||
function filterSubscriberCreatedAtDate() {
|
||||
return array_fill(0, $this->subscribersCount, $this->currentTime);
|
||||
}
|
||||
|
||||
function filterSubscriberState($subscribersData) {
|
||||
if(!in_array('s_status', $this->subscriberFields)) return;
|
||||
$states = array(
|
||||
'subscribed' => array(
|
||||
'subscribed',
|
||||
@ -52,8 +156,7 @@ class Import {
|
||||
'false'
|
||||
)
|
||||
);
|
||||
|
||||
return array_map(function ($state) use ($states) {
|
||||
$subscribersData['s_status'] = array_map(function ($state) use ($states) {
|
||||
if(in_array(strtolower($state), $states['subscribed'])) {
|
||||
return 1;
|
||||
}
|
||||
@ -61,7 +164,92 @@ class Import {
|
||||
return -1;
|
||||
}
|
||||
return 1; // make "subscribed" a default state
|
||||
}, $data);
|
||||
}, $subscribersData['s_status']);
|
||||
return $subscribersData;
|
||||
}
|
||||
|
||||
function addOrUpdateSubscribers($action, $subscribersData, $subscriberFields) {
|
||||
$subscribersCount = count(reset($subscribersData)) - 1;
|
||||
$subscribers = array_map(function ($index) use ($subscribersData, $subscriberFields) {
|
||||
return array_map(function ($field) use ($index, $subscribersData) {
|
||||
return $subscribersData[$field][$index];
|
||||
}, $subscriberFields);
|
||||
}, range(0, $subscribersCount));
|
||||
$subscriberFields = str_replace('s_', '', $subscriberFields);
|
||||
$currentTime = ($action === 'update') ? date('Y-m-d H:i:s') : $this->currentTime;
|
||||
foreach (array_chunk($subscribers, 200) as $data) {
|
||||
try {
|
||||
if($action == 'create') {
|
||||
Subscriber::createMultiple(
|
||||
$subscriberFields,
|
||||
$data
|
||||
);
|
||||
}
|
||||
if($action == 'update') {
|
||||
Subscriber::updateMultiple(
|
||||
$subscriberFields,
|
||||
$data,
|
||||
$currentTime
|
||||
);
|
||||
}
|
||||
} catch (\PDOException $e) {
|
||||
throw new \Exception($e->getMessage());
|
||||
}
|
||||
}
|
||||
$result = Helpers::arrayColumn( // return id=>email array of results
|
||||
Subscriber::selectMany(
|
||||
array(
|
||||
'id',
|
||||
'email'
|
||||
))
|
||||
->where(($action === 'create') ? 'created_at' : 'updated_at', $currentTime)
|
||||
->findArray(),
|
||||
'email', 'id'
|
||||
);
|
||||
if($this->subscriberCustomFields) {
|
||||
$this->addOrUpdateCustomFields(
|
||||
($action === 'create') ? 'create' : 'update',
|
||||
$result,
|
||||
$subscribersData
|
||||
);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
function addOrUpdateCustomFields($action, $dbSubscribers, $subscribersData) {
|
||||
$subscribers = array_map(
|
||||
function ($column) use ($dbSubscribers, $subscribersData) {
|
||||
$count = range(0, count($subscribersData[$column]) - 1);
|
||||
return array_map(
|
||||
function ($index, $value)
|
||||
use ($dbSubscribers, $subscribersData, $column) {
|
||||
$subscriberId = array_search(
|
||||
$subscribersData['s_email'][$index],
|
||||
$dbSubscribers
|
||||
);
|
||||
return array(
|
||||
$column,
|
||||
$subscriberId,
|
||||
$value
|
||||
);
|
||||
}, $count, $subscribersData[$column]);
|
||||
}, $this->subscriberCustomFields)[0];
|
||||
foreach (array_chunk($subscribers, 200) as $data) {
|
||||
try {
|
||||
if($action === 'create') {
|
||||
SubscriberCustomField::createMultiple(
|
||||
$data
|
||||
);
|
||||
}
|
||||
if($action === 'update') {
|
||||
SubscriberCustomField::updateMultiple(
|
||||
$data
|
||||
);
|
||||
}
|
||||
} catch (\PDOException $e) {
|
||||
throw new \Exception($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function timeExecution() {
|
||||
|
@ -47,7 +47,7 @@ class MailChimp {
|
||||
}
|
||||
|
||||
return array(
|
||||
'status' => 'success',
|
||||
'result' => true,
|
||||
'data' => $lists
|
||||
);
|
||||
}
|
||||
@ -108,12 +108,14 @@ class MailChimp {
|
||||
}
|
||||
|
||||
return array(
|
||||
'status' => 'success',
|
||||
'data' => $subscribers,
|
||||
'invalid' => false,
|
||||
'duplicate' => false,
|
||||
'header' => $header,
|
||||
'count' => count($subscribers)
|
||||
'result' => true,
|
||||
'data' => array(
|
||||
'subscribers' => $subscribers,
|
||||
'invalid' => false,
|
||||
'duplicate' => false,
|
||||
'header' => $header,
|
||||
'count' => count($subscribers)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -128,24 +130,24 @@ class MailChimp {
|
||||
|
||||
private function processError($error) {
|
||||
switch ($error) {
|
||||
case 'API':
|
||||
$message = __('Invalid API key.');
|
||||
break;
|
||||
case 'connection':
|
||||
$message = __('Could not connect to your MailChimp account.');
|
||||
break;
|
||||
case 'headers':
|
||||
$message = __('The selected lists do not have matching columns (headers).');
|
||||
break;
|
||||
case 'size':
|
||||
$message = __('Information received from MailChimp is too large for processing. Please limit the number of lists.');
|
||||
break;
|
||||
case 'subscribers':
|
||||
$message = __('Did not find any active subscribers.');
|
||||
break;
|
||||
case 'lists':
|
||||
$message = __('Did not find any valid lists');
|
||||
break;
|
||||
case 'API':
|
||||
$message = __('Invalid API key.');
|
||||
break;
|
||||
case 'connection':
|
||||
$message = __('Could not connect to your MailChimp account.');
|
||||
break;
|
||||
case 'headers':
|
||||
$message = __('The selected lists do not have matching columns (headers).');
|
||||
break;
|
||||
case 'size':
|
||||
$message = __('Information received from MailChimp is too large for processing. Please limit the number of lists.');
|
||||
break;
|
||||
case 'subscribers':
|
||||
$message = __('Did not find any active subscribers.');
|
||||
break;
|
||||
case 'lists':
|
||||
$message = __('Did not find any valid lists');
|
||||
break;
|
||||
}
|
||||
return array(
|
||||
'status' => 'error',
|
||||
|
@ -38,12 +38,12 @@ class CustomField extends Model {
|
||||
}
|
||||
|
||||
function subscribers() {
|
||||
return $this->has_many_through(
|
||||
return $this->hasManyThrough(
|
||||
__NAMESPACE__ . '\Subscriber',
|
||||
__NAMESPACE__ . '\SubscriberCustomField',
|
||||
'custom_field_id',
|
||||
'subscriber_id'
|
||||
)->select_expr(MP_SUBSCRIBER_CUSTOM_FIELD_TABLE.'.value');
|
||||
)->selectExpr(MP_SUBSCRIBER_CUSTOM_FIELD_TABLE . '.value');
|
||||
}
|
||||
|
||||
static function createOrUpdate($data = array()) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
<?php
|
||||
namespace MailPoet\Models;
|
||||
|
||||
use MailPoet\Util\Helpers;
|
||||
if(!defined('ABSPATH')) exit;
|
||||
|
||||
class Subscriber extends Model {
|
||||
@ -302,4 +303,52 @@ class Subscriber extends Model {
|
||||
->whereNull('deleted_at')
|
||||
->where('status', 'unconfirmed');
|
||||
}
|
||||
|
||||
static function createMultiple($columns, $values) {
|
||||
return self::rawExecute(
|
||||
'INSERT IGNORE INTO `' . self::$_table . '` ' .
|
||||
'(' . implode(', ', $columns) . ') ' .
|
||||
'VALUES ' . rtrim(
|
||||
str_repeat(
|
||||
'(' . rtrim(str_repeat('?,', count($columns)), ',') . ')' . ', '
|
||||
, count($values)
|
||||
)
|
||||
, ', '),
|
||||
Helpers::flattenArray($values)
|
||||
);
|
||||
}
|
||||
|
||||
static function updateMultiple($columns, $subscribers, $currentTime) {
|
||||
$ignoreColumnsOnUpdate = array(
|
||||
'email',
|
||||
'created_at'
|
||||
);
|
||||
$emailPosition = array_search('email', $columns);
|
||||
$sql = function ($type) use ($columns, $subscribers, $emailPosition, $ignoreColumnsOnUpdate) {
|
||||
return array_filter(
|
||||
array_map(function ($columnPosition, $columnName) use ($type, $subscribers, $emailPosition, $ignoreColumnsOnUpdate) {
|
||||
if(in_array($columnName, $ignoreColumnsOnUpdate)) return;
|
||||
$query = array_map(
|
||||
function ($subscriber) use ($type, $columnPosition, $emailPosition) {
|
||||
return ($type === 'values') ?
|
||||
array(
|
||||
$subscriber[$emailPosition],
|
||||
$subscriber[$columnPosition]
|
||||
) :
|
||||
'WHEN email = ? THEN ?';
|
||||
}, $subscribers);
|
||||
return ($type === 'values') ?
|
||||
Helpers::flattenArray($query) :
|
||||
$columnName . '= (CASE ' . implode(' ', $query) . ' END)';
|
||||
}, array_keys($columns), $columns)
|
||||
);
|
||||
};
|
||||
return self::rawExecute(
|
||||
'UPDATE `' . self::$_table . '` ' .
|
||||
'SET ' . implode(', ', $sql('statement')) . ', ' .
|
||||
'updated_at = "' . $currentTime . '" ' .
|
||||
'WHERE email IN (' . rtrim(str_repeat('?,', count($subscribers)), ',') . ')',
|
||||
array_merge(Helpers::flattenArray($sql('values')), Helpers::arrayColumn($subscribers, $emailPosition))
|
||||
);
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
<?php
|
||||
namespace MailPoet\Models;
|
||||
|
||||
use MailPoet\Util\Helpers;
|
||||
|
||||
if(!defined('ABSPATH')) exit;
|
||||
|
||||
class SubscriberCustomField extends Model {
|
||||
@ -9,4 +11,38 @@ class SubscriberCustomField extends Model {
|
||||
function __construct() {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
static function createMultiple($values) {
|
||||
return self::rawExecute(
|
||||
'INSERT IGNORE INTO `' . self::$_table . '` ' .
|
||||
'(custom_field_id, subscriber_id, value) ' .
|
||||
'VALUES ' . rtrim(
|
||||
str_repeat(
|
||||
'(?, ?, ?)' . ', '
|
||||
, count($values)
|
||||
), ', '
|
||||
),
|
||||
Helpers::flattenArray($values)
|
||||
);
|
||||
}
|
||||
|
||||
static function updateMultiple($subscribers) {
|
||||
self::createMultiple($subscribers);
|
||||
self::rawExecute(
|
||||
'UPDATE `' . self::$_table . '` ' .
|
||||
'SET value = ' .
|
||||
'(CASE ' .
|
||||
str_repeat(
|
||||
'WHEN custom_field_id = ? AND subscriber_id = ? THEN ? ',
|
||||
count($subscribers)
|
||||
) .
|
||||
'END) ' .
|
||||
'WHERE subscriber_id IN (' .
|
||||
implode(', ', Helpers::arrayColumn($subscribers, 1)) .
|
||||
') AND custom_field_id IN (' .
|
||||
implode(', ', array_unique(Helpers::arrayColumn($subscribers, 0)))
|
||||
. ') ',
|
||||
Helpers::flattenArray($subscribers)
|
||||
);
|
||||
}
|
||||
}
|
@ -21,14 +21,13 @@ class Import {
|
||||
function addSegment($data) {
|
||||
$segment = Segment::createOrUpdate($data, $returnObject = true);
|
||||
wp_send_json(
|
||||
(!is_array($segment)) ?
|
||||
(!is_object($segment)) ?
|
||||
array(
|
||||
'status' => 'error',
|
||||
'message' => $segment
|
||||
'result' => false,
|
||||
) :
|
||||
array(
|
||||
'status' => 'success',
|
||||
'segment' => $segment
|
||||
'result' => true,
|
||||
'segment' => $segment->asArray()
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -40,17 +39,27 @@ class Import {
|
||||
wp_send_json(
|
||||
(!$result) ?
|
||||
array(
|
||||
'status' => 'error'
|
||||
'result' => false
|
||||
) :
|
||||
array(
|
||||
'status' => 'success',
|
||||
'result' => true,
|
||||
'customField' => $customField->asArray()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function process($data) {
|
||||
$data = file_get_contents(dirname(__FILE__) . '/../../export.txt');
|
||||
$import = new \MailPoet\Import\Import(json_decode($data, true));
|
||||
wp_send_json($import->process());
|
||||
try {
|
||||
wp_send_json($import->process());
|
||||
} catch (\Exception $e) {
|
||||
wp_send_json(
|
||||
array(
|
||||
'result' => false,
|
||||
'error' => $e->getMessage()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -75,7 +75,7 @@ class Helpers {
|
||||
|
||||
static function getMaxPostSize($bytes = false) {
|
||||
$maxPostSize = ini_get('post_max_size');
|
||||
if (!$bytes) return $maxPostSize;
|
||||
if(!$bytes) return $maxPostSize;
|
||||
$maxPostSizeBytes = (int) $maxPostSize;
|
||||
$unit = strtolower($maxPostSize[strlen($maxPostSize) - 1]);
|
||||
switch ($unit) {
|
||||
@ -90,8 +90,82 @@ class Helpers {
|
||||
}
|
||||
|
||||
static function flattenArray($array) {
|
||||
return call_user_func_array(
|
||||
'array_merge_recursive', array_map('array_values', $array)
|
||||
);
|
||||
if(!$array) return;
|
||||
$flattened_array = array();
|
||||
array_walk_recursive($array, function ($a) use (&$flattened_array) { $flattened_array[] = $a; });
|
||||
return $flattened_array;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Using func_get_args() in order to check for proper number ofparameters and trigger errors exactly as the built-in array_column()
|
||||
* does in PHP 5.5.
|
||||
* @author Ben Ramsey (http://benramsey.com)
|
||||
*/
|
||||
static function arrayColumn($input = null, $columnKey = null, $indexKey = null) {
|
||||
$argc = func_num_args();
|
||||
$params = func_get_args();
|
||||
if($argc < 2) {
|
||||
trigger_error("array_column() expects at least 2 parameters, {$argc} given", E_USER_WARNING);
|
||||
return null;
|
||||
}
|
||||
if(!is_array($params[0])) {
|
||||
trigger_error(
|
||||
'array_column() expects parameter 1 to be array, ' . gettype($params[0]) . ' given',
|
||||
E_USER_WARNING
|
||||
);
|
||||
return null;
|
||||
}
|
||||
if(!is_int($params[1])
|
||||
&& !is_float($params[1])
|
||||
&& !is_string($params[1])
|
||||
&& $params[1] !== null
|
||||
&& !(is_object($params[1]) && method_exists($params[1], '__toString'))
|
||||
) {
|
||||
trigger_error('array_column(): The column key should be either a string or an integer', E_USER_WARNING);
|
||||
return false;
|
||||
}
|
||||
if(isset($params[2])
|
||||
&& !is_int($params[2])
|
||||
&& !is_float($params[2])
|
||||
&& !is_string($params[2])
|
||||
&& !(is_object($params[2]) && method_exists($params[2], '__toString'))
|
||||
) {
|
||||
trigger_error('array_column(): The index key should be either a string or an integer', E_USER_WARNING);
|
||||
return false;
|
||||
}
|
||||
$paramsInput = $params[0];
|
||||
$paramsColumnKey = ($params[1] !== null) ? (string) $params[1] : null;
|
||||
$paramsIndexKey = null;
|
||||
if(isset($params[2])) {
|
||||
if(is_float($params[2]) || is_int($params[2])) {
|
||||
$paramsIndexKey = (int) $params[2];
|
||||
} else {
|
||||
$paramsIndexKey = (string) $params[2];
|
||||
}
|
||||
}
|
||||
$resultArray = array();
|
||||
foreach ($paramsInput as $row) {
|
||||
$key = $value = null;
|
||||
$keySet = $valueSet = false;
|
||||
if($paramsIndexKey !== null && array_key_exists($paramsIndexKey, $row)) {
|
||||
$keySet = true;
|
||||
$key = (string) $row[$paramsIndexKey];
|
||||
}
|
||||
if($paramsColumnKey === null) {
|
||||
$valueSet = true;
|
||||
$value = $row;
|
||||
} elseif(is_array($row) && array_key_exists($paramsColumnKey, $row)) {
|
||||
$valueSet = true;
|
||||
$value = $row[$paramsColumnKey];
|
||||
}
|
||||
if($valueSet) {
|
||||
if($keySet) {
|
||||
$resultArray[$key] = $value;
|
||||
} else {
|
||||
$resultArray[] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $resultArray;
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@
|
||||
<!-- STEP 2: subscriber data manipulation -->
|
||||
<% include 'import/step2.html' %>
|
||||
<!-- STEP 3: results -->
|
||||
<% include 'import/step3.html' %>
|
||||
</div>
|
||||
|
||||
<%= stylesheet('import.css') %>
|
||||
@ -52,7 +53,9 @@
|
||||
'columnContainsInvalidDate': __('One of the columns contains an invalid date. Please fix before continuing.'),
|
||||
'listCreateError': __('Error adding a new segment:'),
|
||||
'columnContainsInvalidElement': __('One of the columns contains an invalid email. Please fix before continuing.'),
|
||||
'customFieldCreateError': __('Custom field could not be created.')
|
||||
'customFieldCreateError': __('Custom field could not be created.'),
|
||||
'subscribersAdded': __('%1$s subscribers added to %2$s.'),
|
||||
'subscribersUpdated': __('%1$s existing subscribers were updated and added to %2$s.')
|
||||
}) %>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
@ -48,7 +48,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="mailpoet_method_process">
|
||||
<!-- WILL BE INSERTED: Next button & spam notice template -->
|
||||
<!-- Template data -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -82,7 +82,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="mailpoet_method_process">
|
||||
<!-- WILL BE INSERTED: Next button & spam notice template -->
|
||||
<!-- Template data -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -115,7 +115,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="mailpoet_method_process">
|
||||
<!-- WILL BE INSERTED: Next button & spam notice template -->
|
||||
<!-- Template data -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -79,7 +79,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
<a href="javascript:;" id="step_2_process"
|
||||
<a href="javascript:;" id="step2_process"
|
||||
class="button-primary wysija disabled"><%= __('Next step') %> </a>
|
||||
</th>
|
||||
</tr>
|
||||
|
32
views/import/step3.html
Normal file
32
views/import/step3.html
Normal file
@ -0,0 +1,32 @@
|
||||
<div id="step3" class="mailpoet_hidden">
|
||||
<div id="subscribers_data_import_results" class="mailpoet_hidden notice">
|
||||
<!-- Template data -->
|
||||
</div>
|
||||
|
||||
<table class="mailpoet_subscribers form-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<a href="javascript:;"
|
||||
class="button-primary wysija mailpoet_import_again"><%= __('Import again') %></a>
|
||||
<a href="javascript:;"
|
||||
class="button-primary wysija mailpoet_view_subscribers"><%= __('View subscribers') %></a>
|
||||
</th>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<script id="subscribers_data_import_results_template" type="text/x-handlebars-template">
|
||||
<ul>
|
||||
{{#if added}}
|
||||
<li>{{{added}}}</li>
|
||||
{{/if}}
|
||||
{{#if updated}}
|
||||
<li>{{{updated}}}</li>
|
||||
{{/if}}
|
||||
{{#if noaction}}
|
||||
<li><%= __('No new subscribers were found/added.') %></li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
</script>
|
||||
</div>
|
Reference in New Issue
Block a user