- Adds tests for the main Import class

- Updates tests for Env (proper host detection with port)
- Improves import
This commit is contained in:
MrCasual
2015-11-09 00:24:39 -05:00
parent 33ea16eb0f
commit 7b54285ca6
9 changed files with 402 additions and 105 deletions

View File

@@ -646,7 +646,7 @@ define(
columnId = 'ignore'; // set default column type
// if the column is not undefined and has a valid e-mail, set type as email
if (columnData % 1 !== 0 && emailRegex.test(columnData)) {
columnId = 's_email';
columnId = 'email';
} else if (subscribers.header) {
var headerName = subscribers.header[i],
header_name_match = mailpoetColumns.map(function (el) {
@@ -658,15 +658,15 @@ define(
}// set column type using header name
else if (headerName) {
if (/first|first name|given name/i.test(headerName)) {
columnId = 's_first_name';
columnId = 'first_name';
} else if (/last|last name/i.test(headerName)) {
columnId = 's_last_name';
columnId = 'last_name';
} else if (/status/i.test(headerName)) {
columnId = 's_status';
columnId = 'status';
} /*else if (/subscribed|subscription/i.test(headerName)) {
columnId = 's_confirmed_at';
columnId = 'confirmed_at';
} else if (/ip/i.test(headerName)) {
columnId = 's_confirmed_ip';
columnId = 'confirmed_ip';
}*/
}
}
@@ -891,7 +891,7 @@ define(
// EMAIL filter: if the last value in the column doesn't have a valid
// email, hide the next button
if (column.id === "s_email") {
if (column.id === "email") {
if (!emailRegex.test(subscribersClone.subscribers[0][matchedColumn])) {
preventNextStep = true;
if (!jQuery('[data-id="notice_invalidEmail"]').length) {
@@ -1083,9 +1083,9 @@ define(
.html()),
exportMenuElement = jQuery('span.mailpoet_export'),
importResults = {
added: (importData.step2.added)
? MailPoetI18n.subscribersAdded
.replace('%1$s', '<strong>' + importData.step2.added + '</strong>')
created: (importData.step2.created)
? MailPoetI18n.subscribersCreated
.replace('%1$s', '<strong>' + importData.step2.created + '</strong>')
.replace('%2$s', '"' + importData.step2.segments.join('", "') + '"')
: false,
updated: (importData.step2.updated)
@@ -1093,7 +1093,7 @@ define(
.replace('%1$s', '<strong>' + importData.step2.updated + '</strong>')
.replace('%2$s', '"' + importData.step2.segments.join('", "') + '"')
: false,
noaction: (!importData.step2.updated && !importData.step2.added)
noaction: (!importData.step2.updated && !importData.step2.created)
? true
: false
};
@@ -1111,9 +1111,9 @@ define(
window.location.href = 'admin.php?page=mailpoet-subscribers';
});
// if new subscribers were added and the export menu item is hidden
// if new subscribers were created and the export menu item is hidden
// (it's shown only when there are subscribers), display it
if (importResults.added && exportMenuElement.not(':visible')) {
if (importResults.created && exportMenuElement.not(':visible')) {
exportMenuElement.show();
}

View File

@@ -15,12 +15,12 @@ class BootstrapMenu {
function getSubscriberFields() {
return array(
's_email' => __('Email'),
's_first_name' => __('First name'),
's_last_name' => __('Last name'),
's_status' => __('Status')
/* 's_confirmed_ip' => __('IP address')
's_confirmed_at' => __('Subscription date')*/
'email' => __('Email'),
'first_name' => __('First name'),
'last_name' => __('Last name'),
'status' => __('Status')
/* 'confirmed_ip' => __('IP address')
'confirmed_at' => __('Subscription date')*/
);
}
@@ -44,7 +44,7 @@ class BootstrapMenu {
return array(
'id' => $fieldId,
'name' => $fieldName,
'type' => ($fieldId === 's_confirmed_at') ? 'date' : null,
'type' => ($fieldId === 'confirmed_at') ? 'date' : null,
'custom' => false
);
}, array_keys($subscriberFields), $subscriberFields);

View File

@@ -1,7 +1,6 @@
<?php
namespace MailPoet\Import;
use MailPoet\Import\BootstrapMenu;
use MailPoet\Models\Subscriber;
use MailPoet\Models\SubscriberCustomField;
use MailPoet\Models\SubscriberSegment;
@@ -12,8 +11,12 @@ class Import {
$this->subscribersData = $data['subscribers'];
$this->segments = $data['segments'];
$this->updateSubscribers = $data['updateSubscribers'];
$this->subscriberFields = $this->getSubscriberFields();
$this->subscriberCustomFields = $this->getCustomSubscriberFields();
$this->subscriberFields = $this->getSubscriberFields(
array_keys($this->subscribersData)
);
$this->subscriberCustomFields = $this->getCustomSubscriberFields(
array_keys($this->subscribersData)
);
$this->subscribersCount = count(reset($this->subscribersData));
$this->currentTime = date('Y-m-d H:i:s');
$this->profilerStart = microtime(true);
@@ -21,35 +24,39 @@ class Import {
function process() {
$subscriberFields = $this->subscriberFields;
$subscriberCustomFields = $this->subscriberCustomFields;
$subscribersData = $this->subscribersData;
$subscribersData = $this->filterSubscriberState($subscribersData);
$subscribersData = $this->filterSubscriberStatus($subscribersData);
list($subscribersData, $subscriberFields) = $this->extendSubscribersAndFields(
$subscribersData, $subscriberFields
);
list($existingSubscribers, $newSubscribers) =
$this->filterExistingAndNewSubscribers($subscribersData);
$addedSubscribers = $updatedSubscribers = array();
$createdSubscribers = $updatedSubscribers = array();
try {
if($newSubscribers) {
$addedSubscribers = $this->addOrUpdateSubscribers(
$createdSubscribers =
$this->createOrUpdateSubscribers(
'create',
$newSubscribers,
$subscriberFields
$subscriberFields,
$subscriberCustomFields
);
$this->addSubscribersToSegments(array_keys($addedSubscribers));
}
if($existingSubscribers && $this->updateSubscribers) {
$updatedSubscribers = $this->addOrUpdateSubscribers(
$updatedSubscribers =
$this->createOrUpdateSubscribers(
'update',
$existingSubscribers,
$subscriberFields
$subscriberFields,
$subscriberCustomFields
);
$this->addSubscribersToSegments(array_keys($updatedSubscribers));
if($addedSubscribers) {
if($createdSubscribers) {
// subtract added from updated subscribers when DB operation takes <1s
$updatedSubscribers = array_diff_key(
$updatedSubscribers,
$addedSubscribers
$createdSubscribers,
$subscriberCustomFields
);
}
}
@@ -63,7 +70,7 @@ class Import {
return array(
'result' => true,
'data' => array(
'added' => count($addedSubscribers),
'created' => count($createdSubscribers),
'updated' => count($updatedSubscribers),
'segments' => $segments->getSegments()
),
@@ -77,7 +84,7 @@ class Import {
return Subscriber::selectMany(array('email'))
->whereIn('email', $subscriberEmails)
->findArray();
}, array_chunk($subscribersData['s_email'], 200))
}, array_chunk($subscribersData['email'], 200))
);
if(!$existingRecords) {
return array(
@@ -88,7 +95,7 @@ class Import {
$existingRecords = Helpers::flattenArray($existingRecords);
$newRecords = array_keys(
array_diff(
$subscribersData['s_email'],
$subscribersData['email'],
$existingRecords
)
);
@@ -132,19 +139,23 @@ class Import {
);
}
function getSubscriberFields() {
return array_filter(
function getSubscriberFields($subscriberFields) {
return array_values(
array_filter(
array_map(function ($field) {
if(!is_int($field)) return $field;
}, array_keys($this->subscribersData))
}, $subscriberFields)
)
);
}
function getCustomSubscriberFields() {
return array_filter(
function getCustomSubscriberFields($subscriberFields) {
return array_values(
array_filter(
array_map(function ($field) {
if(is_int($field)) return $field;
}, array_keys($this->subscribersData))
}, $subscriberFields)
)
);
}
@@ -152,9 +163,9 @@ class Import {
return array_fill(0, $this->subscribersCount, $this->currentTime);
}
function filterSubscriberState($subscribersData) {
if(!in_array('s_status', $this->subscriberFields)) return;
$states = array(
function filterSubscriberStatus($subscribersData) {
if(!in_array('status', $this->subscriberFields)) return;
$statuses = array(
'subscribed' => array(
'subscribed',
'confirmed',
@@ -169,26 +180,30 @@ class Import {
'false'
)
);
$subscribersData['s_status'] = array_map(function ($state) use ($states) {
if(in_array(strtolower($state), $states['subscribed'])) {
$subscribersData['status'] = array_map(function ($state) use ($statuses) {
if(in_array(strtolower($state), $statuses['subscribed'])) {
return 1;
}
if(in_array(strtolower($state), $states['unsubscribed'])) {
if(in_array(strtolower($state), $statuses['unsubscribed'])) {
return -1;
}
return 1; // make "subscribed" a default state
}, $subscribersData['s_status']);
return 1; // make "subscribed" a default status
}, $subscribersData['status']);
return $subscribersData;
}
function addOrUpdateSubscribers($action, $subscribersData, $subscriberFields) {
function createOrUpdateSubscribers(
$action,
$subscribersData,
$subscriberFields,
$subscriberCustomFields
) {
$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) {
if($action == 'create') {
@@ -215,17 +230,27 @@ class Import {
->findArray(),
'email', 'id'
);
if($this->subscriberCustomFields) {
$this->addOrUpdateCustomFields(
if($subscriberCustomFields) {
$this->createOrUpdateCustomFields(
($action === 'create') ? 'create' : 'update',
$result,
$subscribersData
$subscribersData,
$subscriberCustomFields
);
}
$this->addSubscribersToSegments(
array_keys($result),
$this->segments
);
return $result;
}
function addOrUpdateCustomFields($action, $dbSubscribers, $subscribersData) {
function createOrUpdateCustomFields(
$action,
$dbSubscribers,
$subscribersData,
$subscriberCustomFields
) {
$subscribers = array_map(
function ($column) use ($dbSubscribers, $subscribersData) {
$count = range(0, count($subscribersData[$column]) - 1);
@@ -233,7 +258,7 @@ class Import {
function ($index, $value)
use ($dbSubscribers, $subscribersData, $column) {
$subscriberId = array_search(
$subscribersData['s_email'][$index],
$subscribersData['email'][$index],
$dbSubscribers
);
return array(
@@ -242,7 +267,7 @@ class Import {
$value
);
}, $count, $subscribersData[$column]);
}, $this->subscriberCustomFields)[0];
}, $subscriberCustomFields)[0];
foreach (array_chunk($subscribers, 200) as $data) {
if($action === 'create') {
SubscriberCustomField::createMultiple(
@@ -257,9 +282,9 @@ class Import {
}
}
function addSubscribersToSegments($subscribers) {
function addSubscribersToSegments($subscribers, $segments) {
foreach (array_chunk($subscribers, 200) as $data) {
SubscriberSegment::createMultiple($this->segments, $data);
SubscriberSegment::createMultiple($segments, $data);
}
}

View File

@@ -1,48 +1,62 @@
<?php
use \MailPoet\Config\Env;
use MailPoet\Config\Env;
class EnvCest {
function _before() {
Env::init('file', '1.0.0');
}
function itCanReturnThePluginPrefix() {
function itCanReturnPluginPrefix() {
expect(Env::$plugin_prefix)->equals('mailpoet_');
}
function itCanReturnTheDbPrefix() {
function itCanReturnDbPrefix() {
global $wpdb;
$db_prefix = $wpdb->prefix . 'mailpoet_';
expect(Env::$db_prefix)->equals($db_prefix);
}
function itCanReturnTheDbHost() {
expect(Env::$db_host)->equals(DB_HOST);
function itCanReturnDbHost() {
if(preg_match('/(?=:\d+$)/', DB_HOST)) {
expect(Env::$db_host)->equals(explode(':', DB_HOST)[0]);
} else expect(Env::$db_host)->equals(DB_HOST);
}
function itCanReturnTheDbName() {
function itCanReturnDbPort() {
if(preg_match('/(?=:\d+$)/', DB_HOST)) {
expect(Env::$db_port)->equals(explode(':', DB_HOST)[1]);
} else expect(Env::$db_port)->equals(3306);
}
function itCanReturnSocket() {
if(!preg_match('/(?=:\d+$)/', DB_HOST)
&& preg_match('/:/', DB_HOST)
) {
expect(Env::$db_socket)->true();
} else expect(Env::$db_socket)->false();
}
function itCanReturnDbName() {
expect(Env::$db_name)->equals(DB_NAME);
}
function itCanReturnTheDbUser() {
function itCanReturnDbUser() {
expect(Env::$db_username)->equals(DB_USER);
}
function itCanReturnTheDbPassword() {
function itCanReturnDbPassword() {
expect(Env::$db_password)->equals(DB_PASSWORD);
}
function itCanReturnTheDbCharset() {
function itCanReturnDbCharset() {
global $wpdb;
$charset = $wpdb->get_charset_collate();
expect(Env::$db_charset)->equals($charset);
}
function itCanGenerateTheDbSourceName() {
$source_name = 'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME;
function itCanGenerateDbSourceName() {
$source_name = ((!ENV::$db_socket) ? 'mysql:host=' : 'mysql:unix_socket=') .
ENV::$db_host . ';port=' . ENV::$db_port . ';dbname=' . DB_NAME;
expect(Env::$db_source_name)->equals($source_name);
}
function _after() {
}
}

View File

@@ -34,6 +34,9 @@ class BootStrapMenuCest {
'name' => 'DOB',
'type' => 'date',
);
$customField = CustomField::create();
$customField->hydrate($this->customFieldsData);
$customField->save();
$this->bootStrapMenu = new BootstrapMenu();
}
@@ -49,10 +52,10 @@ class BootStrapMenuCest {
function itCanGetSubscriberFields() {
$subsriberFields = $this->bootStrapMenu->getSubscriberFields();
$fields = array(
's_email',
's_first_name',
's_last_name',
's_status'
'email',
'first_name',
'last_name',
'status'
);
foreach ($fields as $field) {
expect(in_array($field, array_keys($subsriberFields)))->true();
@@ -78,9 +81,6 @@ class BootStrapMenuCest {
}
function itCanGetSubsciberCustomFields() {
$customField = CustomField::create();
$customField->hydrate($this->customFieldsData);
$customField->save();
$subscriberCustomFields =
$this->bootStrapMenu
->getSubscriberCustomFields();
@@ -89,9 +89,6 @@ class BootStrapMenuCest {
}
function itCanFormatSubsciberCustomFields() {
$customField = CustomField::create();
$customField->hydrate($this->customFieldsData);
$customField->save();
$formattedSubscriberCustomFields =
$this->bootStrapMenu->formatSubscriberCustomFields(
$this->bootStrapMenu->getSubscriberCustomFields()
@@ -111,7 +108,7 @@ class BootStrapMenuCest {
function itCanFormatFieldsForSelect2() {
$bootStrapMenu = clone($this->bootStrapMenu);
$select2FieldsWithouCustomFields = array(
$select2FieldsWithoutCustomFields = array(
array(
'name' => 'Actions',
'children' => array(
@@ -133,7 +130,7 @@ class BootStrapMenuCest {
)
);
$select2FieldsWithCustomFields = array_merge(
$select2FieldsWithouCustomFields,
$select2FieldsWithoutCustomFields,
array(
array(
'name' => __('User columns'),
@@ -152,10 +149,13 @@ class BootStrapMenuCest {
$bootStrapMenu->subscriberFields,
$bootStrapMenu->subscriberCustomFields
);
expect($formattedFieldsForSelect2)->equals($select2FieldsWithouCustomFields);
expect($formattedFieldsForSelect2)->equals($select2FieldsWithoutCustomFields);
}
function itCanBootstrap() {
$customField = CustomField::create();
$customField->hydrate($this->customFieldsData);
$customField->save();
$bootstrap = clone($this->bootStrapMenu);
$this->_createSegmentsAndSubscribers();
$bootstrap->segments = $bootstrap->getSegments();
@@ -191,6 +191,8 @@ class BootStrapMenuCest {
function _after() {
ORM::forTable(Subscriber::$_table)
->deleteMany();
ORM::forTable(CustomField::$_table)
->deleteMany();
ORM::forTable(Segment::$_table)
->deleteMany();
ORM::forTable(SubscriberSegment::$_table)

View File

@@ -1,6 +1,262 @@
<?php
use MailPoet\Import\Import;
use MailPoet\Models\Subscriber;
use MailPoet\Models\SubscriberCustomField;
use MailPoet\Models\SubscriberSegment;
use MailPoet\Util\Helpers;
class ImportCest {
function _before() {
function __construct() {
$this->JSONdata = json_decode(file_get_contents(dirname(__FILE__) . '/ImportTestData.json'), true);
$this->subscribersData = array(
'first_name' => array(
'Adam',
'Mary'
),
'last_name' => array(
'Smith',
'Jane'
),
'email' => array(
'adam@smith.com',
'mary@jane.com'
),
777 => array(
'France',
'Brazil'
)
);
$this->subscriberFields = array(
'first_name',
'last_name',
'email'
);
$this->segments = range(0, 1);
$this->subscriberCustomFields = array(777);
$this->import = new Import($this->JSONdata);
}
function itCanConstruct() {
expect($this->import->subscribersData)->equals($this->JSONdata['subscribers']);
expect($this->import->segments)->equals($this->JSONdata['segments']);
expect(is_array($this->import->subscriberFields))->true();
expect(is_array($this->import->subscriberCustomFields))->true();
expect($this->import->subscribersCount)->equals(
count($this->JSONdata['subscribers']['email'])
);
expect(
preg_match(
'/\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}/',
$this->import->currentTime)
)->equals(1);
}
function itCanFilterExistingAndNewSubscribers() {
$subscriber = Subscriber::create();
$subscriber->hydrate(
array(
'first_name' => 'Adam',
'last_name' => 'Smith',
'email' => 'adam@smith.com'
));
$subscriber->save();
list($existing, $new) = $this->import->filterExistingAndNewSubscribers(
$this->subscribersData
);
expect($existing['email'][0])->equals($this->subscribersData['email'][0]);
expect($new['email'][0])->equals($this->subscribersData['email'][1]);
}
function itCanExtendSubscribersAndFields() {
expect(in_array('created_at', $this->import->subscriberFields))->false();
expect(isset($this->import->subscriberFields['created_at']))->false();
list($subscribers, $fields) = $this->import->extendSubscribersAndFields(
$this->import->subscribersData,
$this->import->subscriberFields
);
expect(in_array('created_at', $fields))->true();
expect(isset($this->import->subscriberFields['created_at']))->false();
expect(count($subscribers['created_at']))
->equals($this->import->subscribersCount);
}
function itCanGetSubscriberFields() {
$data = array(
'one',
'two',
39
);
$fields = $this->import->getSubscriberFields($data);
expect($fields)->equals(
array(
'one',
'two'
));
}
function itCanGetCustomSubscriberFields() {
$data = array(
'one',
'two',
39
);
$fields = $this->import->getCustomSubscriberFields($data);
expect($fields)->equals(array(39));
}
function itCanFilterSubscriberState() {
$data = array(
'status' => array(
'confirmed',
'subscribed',
'subscribed',
'confirmed',
1,
'1',
'true',
'unsubscribed',
-1,
'-1',
'false',
'something',
'else'
),
);
$statuses = $this->import->filterSubscriberStatus($data);
expect($statuses)->equals(
array(
'status' => array(
1,
1,
1,
1,
1,
1,
1,
-1,
-1,
-1,
-1,
1,
1
)
)
);
}
function itCanAddOrUpdateSubscribers() {
$subscribersData = $this->subscribersData;
$this->import->createOrUpdateSubscribers(
'create',
$subscribersData,
$this->subscriberFields,
false
);
$subscribers = Subscriber::findArray();
expect(count($subscribers))->equals(2);
expect($subscribers[0]['email'])
->equals($subscribersData['email'][0]);
$data['first_name'][1] = 'MaryJane';
$this->import->createOrUpdateSubscribers(
'update',
$subscribersData,
$this->subscriberFields,
false
);
$subscribers = Subscriber::findArray();
expect($subscribers[1]['first_name'])
->equals($subscribersData['first_name'][1]);
}
function itCanCreateOrUpdateCustomFields() {
$subscribersData = $this->subscribersData;
$this->import->createOrUpdateSubscribers(
'create',
$subscribersData,
$this->subscriberFields,
false
);
$dbSubscribers = Helpers::arrayColumn(
Subscriber::selectMany(
array(
'id',
'email'
))
->findArray(),
'email', 'id'
);
$this->import->createOrUpdateCustomFields(
'create',
$dbSubscribers,
$subscribersData,
$this->subscriberCustomFields
);
$subscriberCustomFields = SubscriberCustomField::findArray();
expect(count($subscriberCustomFields))->equals(2);
expect($subscriberCustomFields[0]['value'])
->equals($subscribersData[777][0]);
$subscribersData[777][1] = 'Rio';
$this->import->createOrUpdateCustomFields(
'update',
$dbSubscribers,
$subscribersData,
$this->subscriberCustomFields
);
$subscriberCustomFields = SubscriberCustomField::findArray();
expect($subscriberCustomFields[1]['value'])
->equals($subscribersData[777][1]);
}
function itCanaddSubscribersToSegments() {
$subscribersData = $this->subscribersData;
$this->import->createOrUpdateSubscribers(
'create',
$subscribersData,
$this->subscriberFields,
false
);
$dbSubscribers = Helpers::arrayColumn(
Subscriber::select('id')
->findArray(),
'id'
);
$this->import->addSubscribersToSegments(
$dbSubscribers,
$this->segments
);
$subscribersSegments = SubscriberSegment::findArray();
// 2 subscribers * 2 segments
expect(count($subscribersSegments))->equals(4);
}
function itCanProcess() {
$import = clone($this->import);
$result = $import->process();
expect($result['data']['created'])->equals(997);
expect($result['data']['updated'])->equals(0);
$result = $import->process();
expect($result['data']['created'])->equals(0);
expect($result['data']['updated'])->equals(997);
Subscriber::where('email', 'mbanks4@blinklist.com')
->findOne()
->delete();
$import->currentTime = date('Y-m-d 12:i:s');
$result = $import->process();
expect($result['data']['created'])->equals(1);
expect($result['data']['updated'])->equals(996);
$import->updateSubscribers = false;
$result = $import->process();
expect($result['data']['created'])->equals(0);
expect($result['data']['updated'])->equals(0);
}
function _after() {
ORM::forTable(Subscriber::$_table)
->deleteMany();
ORM::forTable(SubscriberCustomField::$_table)
->deleteMany();
ORM::forTable(SubscriberSegment::$_table)
->deleteMany();
}
}

View File

@@ -999,7 +999,7 @@
"China",
"China"
],
"s_first_name":[
"first_name":[
"Rose",
"Samuel",
"Louis",
@@ -1998,7 +1998,7 @@
"Lillian",
"Ryan"
],
"s_last_name":[
"last_name":[
"Hansen",
"Hall",
"Freeman",
@@ -2997,7 +2997,7 @@
"Clark",
"Russell"
],
"s_email":[
"email":[
"rhansen0@stanford.edu",
"shall1@zdnet.com",
"lfreeman2@gmpg.org",
@@ -3996,7 +3996,7 @@
"lclarkrq@xinhuanet.com",
"rrussellrr@bloomberg.com"
],
"s_status":[
"status":[
"unsubscribed",
"confirmed",
"subscribed",

View File

@@ -54,7 +54,7 @@
'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.'),
'subscribersAdded': __('%1$s subscribers added to %2$s.'),
'subscribersCreated': __('%1$s subscribers added to %2$s.'),
'subscribersUpdated': __('%1$s existing subscribers were updated and added to %2$s.')
}) %>

View File

@@ -19,8 +19,8 @@
<script id="subscribers_data_import_results_template" type="text/x-handlebars-template">
<ul>
{{#if added}}
<li>{{{added}}}</li>
{{#if created}}
<li>{{{created}}}</li>
{{/if}}
{{#if updated}}
<li>{{{updated}}}</li>