mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-20 22:42:53 -05:00
Integrated @tryghost/members-importer
closes https://github.com/TryGhost/Team/issues/916 - The members importer module was extracted into an ouside module as per project structuring standards
This commit is contained in:
parent
49f48c0f09
commit
57c4afdea2
6 changed files with 13 additions and 497 deletions
|
@ -1,178 +0,0 @@
|
|||
const iff = (cond, yes, no) => (cond ? yes : no);
|
||||
module.exports = ({result, siteUrl, membersUrl, emailRecipient}) => `
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<title>Your member import is complete</title>
|
||||
<style>
|
||||
/* -------------------------------------
|
||||
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||
------------------------------------- */
|
||||
@media only screen and (max-width: 620px) {
|
||||
table[class=body] h1 {
|
||||
font-size: 28px !important;
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
table[class=body] p,
|
||||
table[class=body] ul,
|
||||
table[class=body] ol,
|
||||
table[class=body] td,
|
||||
table[class=body] span,
|
||||
table[class=body] a {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
table[class=body] .title {
|
||||
font-size: 22px !important;
|
||||
}
|
||||
table[class=body] .wrapper,
|
||||
table[class=body] .article {
|
||||
padding: 10px !important;
|
||||
}
|
||||
table[class=body] .content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
table[class=body] .container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .main {
|
||||
border-left-width: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border-right-width: 0 !important;
|
||||
}
|
||||
table[class=body] .btn table {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .btn a {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .img-responsive {
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: auto !important;
|
||||
}
|
||||
table[class=body] p[class=small],
|
||||
table[class=body] a[class=small] {
|
||||
font-size: 12x !important;
|
||||
}
|
||||
}
|
||||
/* -------------------------------------
|
||||
PRESERVE THESE STYLES IN THE HEAD
|
||||
------------------------------------- */
|
||||
@media all {
|
||||
.ExternalClass {
|
||||
width: 100%;
|
||||
}
|
||||
.ExternalClass,
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
line-height: 100%;
|
||||
}
|
||||
.recipient-link a {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
#MessageViewBody a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
}
|
||||
hr {
|
||||
border-width: 0;
|
||||
height: 0;
|
||||
margin-top: 34px;
|
||||
margin-bottom: 34px;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: #EEF5F8;
|
||||
}
|
||||
a {
|
||||
color: #3A464C;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="" style="background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.5em; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
|
||||
<tr>
|
||||
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;"> </td>
|
||||
<td class="container" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; display: block; Margin: 0 auto; max-width: 540px; padding: 10px; width: 540px;">
|
||||
<div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 600px; padding: 30px 20px;">
|
||||
|
||||
<!-- START CENTERED CONTAINER -->
|
||||
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">Your member import is complete</span>
|
||||
<table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 8px;">
|
||||
|
||||
<!-- START MAIN CONTENT AREA -->
|
||||
<tr>
|
||||
<td class="wrapper" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; box-sizing: border-box;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
|
||||
<tr>
|
||||
<td align="center" style="padding-top: 20px; padding-bottom: 12px;"><img src="https://static.ghost.org/v4.0.0/images/ghost-orb-4.png" width="60" height="60" style="width: 60px; height: 60px;" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; vertical-align: top;">
|
||||
<p class="title" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 21px; color: #3A464C; font-weight: normal; line-height: 25px; margin-bottom: 0px; margin-top: 50px; font-weight: 600; color: #15212A;">${iff(result.imported > 0, `Your member import is complete`, `Your member import was unsuccessful`)}</p>
|
||||
</td>
|
||||
</tr>
|
||||
${iff(result.imported === 0 && result.errors.length === 0, `
|
||||
<tr>
|
||||
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 0px;">No members were added.</p>
|
||||
</td>
|
||||
</tr>
|
||||
`, ``)}
|
||||
${iff(result.imported > 0, `
|
||||
<tr>
|
||||
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 0px;">A total of <strong style="font-weight: 600;">${result.imported}</strong> ${iff(result.imported === 1, 'person', 'people')} were successfully added or updated in your list of members, and now have access to your site.</p>
|
||||
</td>
|
||||
</tr>`, ``)}
|
||||
${iff(result.errors.length > 0, `
|
||||
<tr>
|
||||
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; padding-top: 24px; padding-bottom: 10px;">
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 0px;">
|
||||
${iff(result.imported === 0, `No members were added.`, `<strong style="font-weight: 600;">${result.errors.length}</strong> ${iff(result.errors.length === 1, `member was`, `members were`)} skipped due to errors.`)} There's a validated CSV file attached to this email with the list of errors so that you can fix them and re-upload the CSV to complete the import.</p>
|
||||
</td>
|
||||
</tr>`, '')}
|
||||
<tr>
|
||||
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; padding-bottom: 12px; padding-top: 30px;">
|
||||
<a href="${membersUrl.href}" target="_blank" style="display: inline-block; color: #ffffff; background-color: #15212A; border: solid 1px #15212A; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 16px; font-weight: normal; margin: 0; padding: 9px 22px 10px; border-color: #15212A;">${iff(result.imported > 0, `View members`, `Try again`)}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; vertical-align: top; padding-top: 80px; padding-bottom: 10px;">
|
||||
<div class="footer">
|
||||
<p class="small" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; color: #738A94; font-weight: normal; margin: 0; line-height: 18px; margin-bottom: 0px; font-size: 11px;">This email was sent from <a href="${siteUrl.href}" style="color: #738A94;">${siteUrl.host}</a> to <a href="mailto:${emailRecipient}" style="color: #738A94;">${emailRecipient}</a></p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- END MAIN CONTENT AREA -->
|
||||
</table>
|
||||
|
||||
|
||||
<!-- END CENTERED CONTAINER -->
|
||||
</div>
|
||||
</td>
|
||||
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;"> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
|
@ -1,316 +0,0 @@
|
|||
const moment = require('moment-timezone');
|
||||
const path = require('path');
|
||||
const fs = require('fs-extra');
|
||||
const membersCSV = require('@tryghost/members-csv');
|
||||
const errors = require('@tryghost/errors');
|
||||
const tpl = require('@tryghost/tpl');
|
||||
|
||||
const emailTemplate = require('./email-template');
|
||||
|
||||
const messages = {
|
||||
filenameCollision: 'Filename already exists, please try again.',
|
||||
jobAlreadyComplete: 'Job is already complete.'
|
||||
};
|
||||
|
||||
module.exports = class MembersCSVImporter {
|
||||
/**
|
||||
* @param {Object} options
|
||||
* @param {string} options.storagePath - The path to store CSV's in before importing
|
||||
* @param {Function} options.getTimezone - function returning currently configured timezone
|
||||
* @param {() => Object} options.getMembersApi
|
||||
* @param {Function} options.sendEmail - function sending an email
|
||||
* @param {(string) => boolean} options.isSet - Method checking if specific feature is enabled
|
||||
* @param {({name, at, job, data, offloaded}) => void} options.addJob - Method registering an async job
|
||||
* @param {Object} options.knex - An instance of the Ghost Database connection
|
||||
* @param {Function} options.urlFor - function generating urls
|
||||
*/
|
||||
constructor({storagePath, getTimezone, getMembersApi, sendEmail, isSet, addJob, knex, urlFor}) {
|
||||
this._storagePath = storagePath;
|
||||
this._getTimezone = getTimezone;
|
||||
this._getMembersApi = getMembersApi;
|
||||
this._sendEmail = sendEmail;
|
||||
this._isSet = isSet;
|
||||
this._addJob = addJob;
|
||||
this._knex = knex;
|
||||
this._urlFor = urlFor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {string} JobID
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} Job
|
||||
* @prop {string} filename
|
||||
* @prop {JobID} id
|
||||
* @prop {string} status
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the Job for a jobCode
|
||||
* @param {JobID} jobId
|
||||
* @returns {Promise<Job>}
|
||||
*/
|
||||
async getJob(jobId) {
|
||||
return {
|
||||
id: jobId,
|
||||
filename: jobId,
|
||||
status: 'pending'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a CSV file for import
|
||||
* - Maps headers based on headerMapping, this allows for a non standard CSV
|
||||
* to be imported, so long as a mapping exists between it and a standard CSV
|
||||
* - Stores the CSV to be imported in the storagePath
|
||||
* - Creates a MemberImport Job and associated MemberImportBatch's
|
||||
*
|
||||
* @param {string} inputFilePath - The path to the CSV to prepare
|
||||
* @param {Object.<string, string>} headerMapping - An object whos keys are headers in the input CSV and values are the header to replace it with
|
||||
* @param {Array<string>} defaultLabels - A list of labels to apply to every member
|
||||
*
|
||||
* @returns {Promise<{id: JobID, batches: number, metadata: Object.<string, any>}>} - A promise resolving to the id of the MemberImport Job
|
||||
*/
|
||||
async prepare(inputFilePath, headerMapping, defaultLabels) {
|
||||
const batchSize = 1;
|
||||
|
||||
const siteTimezone = this._getTimezone();
|
||||
const currentTime = moment().tz(siteTimezone).format('YYYY-MM-DD HH:mm:ss.SSS');
|
||||
const outputFileName = `Members Import ${currentTime}.csv`;
|
||||
const outputFilePath = path.join(this._storagePath, '/', outputFileName);
|
||||
|
||||
const pathExists = await fs.pathExists(outputFilePath);
|
||||
|
||||
if (pathExists) {
|
||||
throw new errors.DataImportError(tpl(messages.filenameCollision));
|
||||
}
|
||||
|
||||
const rows = await membersCSV.parse(inputFilePath, headerMapping, defaultLabels);
|
||||
const numberOfBatches = Math.ceil(rows.length / batchSize);
|
||||
const mappedCSV = membersCSV.unparse(rows);
|
||||
|
||||
const hasStripeData = rows.find(function rowHasStripeData(row) {
|
||||
return !!row.stripe_customer_id || !!row.complimentary_plan;
|
||||
});
|
||||
|
||||
await fs.writeFile(outputFilePath, mappedCSV);
|
||||
|
||||
return {
|
||||
id: outputFilePath,
|
||||
batches: numberOfBatches,
|
||||
metadata: {
|
||||
hasStripeData
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an import of a CSV file
|
||||
*
|
||||
* @param {JobID} id - The id of the job to perform
|
||||
*/
|
||||
async perform(id) {
|
||||
const job = await this.getJob(id);
|
||||
|
||||
if (job.status === 'complete') {
|
||||
throw new errors.BadRequestError(tpl(messages.jobAlreadyComplete));
|
||||
}
|
||||
|
||||
const rows = membersCSV.parse(job.filename);
|
||||
|
||||
const membersApi = await this._getMembersApi();
|
||||
|
||||
const defaultProductPage = await membersApi.productRepository.list({
|
||||
limit: 1
|
||||
});
|
||||
|
||||
const defaultProduct = defaultProductPage.data[0];
|
||||
|
||||
const result = await rows.reduce(async (resultPromise, row) => {
|
||||
const resultAccumulator = await resultPromise;
|
||||
|
||||
const trx = await this._knex.transaction();
|
||||
const options = {
|
||||
transacting: trx
|
||||
};
|
||||
|
||||
try {
|
||||
const existingMember = await membersApi.members.get({email: row.email}, {
|
||||
...options,
|
||||
withRelated: ['labels']
|
||||
});
|
||||
let member;
|
||||
if (existingMember) {
|
||||
const existingLabels = existingMember.related('labels') ? existingMember.related('labels').toJSON() : [];
|
||||
member = await membersApi.members.update({
|
||||
...row,
|
||||
labels: existingLabels.concat(row.labels)
|
||||
}, {
|
||||
...options,
|
||||
id: existingMember.id
|
||||
});
|
||||
} else {
|
||||
member = await membersApi.members.create(row, options);
|
||||
}
|
||||
|
||||
if (row.stripe_customer_id) {
|
||||
await membersApi.members.linkStripeCustomer({
|
||||
customer_id: row.stripe_customer_id,
|
||||
member_id: member.id
|
||||
}, options);
|
||||
} else if (row.complimentary_plan) {
|
||||
if (!this._isSet('multipleProducts')) {
|
||||
await membersApi.members.setComplimentarySubscription(member, options);
|
||||
} else if (!row.products) {
|
||||
await membersApi.members.update({
|
||||
products: [{id: defaultProduct.id}]
|
||||
}, {
|
||||
...options,
|
||||
id: member.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (this._isSet('multipleProducts')) {
|
||||
if (row.products) {
|
||||
await membersApi.members.update({
|
||||
products: row.products
|
||||
}, {
|
||||
...options,
|
||||
id: member.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await trx.commit();
|
||||
return {
|
||||
...resultAccumulator,
|
||||
imported: resultAccumulator.imported + 1
|
||||
};
|
||||
} catch (error) {
|
||||
// The model layer can sometimes throw arrays of errors
|
||||
const errorList = [].concat(error);
|
||||
const errorMessage = errorList.map(({message}) => message).join(', ');
|
||||
await trx.rollback();
|
||||
return {
|
||||
...resultAccumulator,
|
||||
errors: [...resultAccumulator.errors, {
|
||||
...row,
|
||||
error: errorMessage
|
||||
}]
|
||||
};
|
||||
}
|
||||
}, Promise.resolve({
|
||||
imported: 0,
|
||||
errors: []
|
||||
}));
|
||||
|
||||
return {
|
||||
total: result.imported + result.errors.length,
|
||||
...result
|
||||
};
|
||||
}
|
||||
|
||||
generateCompletionEmail(result, data) {
|
||||
const siteUrl = new URL(this._urlFor('home', null, true));
|
||||
const membersUrl = new URL('members', this._urlFor('admin', null, true));
|
||||
if (data.importLabel) {
|
||||
membersUrl.searchParams.set('label', data.importLabel.slug);
|
||||
}
|
||||
return emailTemplate({result, siteUrl, membersUrl, ...data});
|
||||
}
|
||||
|
||||
generateErrorCSV(result) {
|
||||
const errorsWithFormattedMessages = result.errors.map((row) => {
|
||||
const formattedError = row.error
|
||||
.replace(
|
||||
'Value in [members.email] cannot be blank.',
|
||||
'Missing email address'
|
||||
)
|
||||
.replace(
|
||||
'Value in [members.note] exceeds maximum length of 2000 characters.',
|
||||
'"Note" exceeds maximum length of 2000 characters'
|
||||
)
|
||||
.replace(
|
||||
'Value in [members.subscribed] must be one of true, false, 0 or 1.',
|
||||
'Value in "Subscribed to emails" must be "true" or "false"'
|
||||
)
|
||||
.replace(
|
||||
'Validation (isEmail) failed for email',
|
||||
'Invalid email address'
|
||||
)
|
||||
.replace(
|
||||
/No such customer:[^,]*/,
|
||||
'Could not find Stripe customer'
|
||||
);
|
||||
|
||||
return {
|
||||
...row,
|
||||
error: formattedError
|
||||
};
|
||||
});
|
||||
return membersCSV.unparse(errorsWithFormattedMessages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes CSV file and imports member&label records depending on the size of the imported set
|
||||
*
|
||||
* @param {Object} config
|
||||
* @param {String} config.pathToCSV - path where imported csv with members records is stored
|
||||
* @param {Object} config.headerMapping - mapping of CSV headers to member record fields
|
||||
* @param {Object} [config.globalLabels] - labels to be applied to whole imported members set
|
||||
* @param {Object} config.importLabel -
|
||||
* @param {String} config.importLabel.name - label name
|
||||
* @param {Object} config.user
|
||||
* @param {String} config.user.email - calling user email
|
||||
* @param {Object} config.LabelModel - instance of Ghosts Label model
|
||||
*/
|
||||
async process({pathToCSV, headerMapping, globalLabels, importLabel, user, LabelModel}) {
|
||||
const job = await this.prepare(pathToCSV, headerMapping, globalLabels);
|
||||
|
||||
if (job.batches <= 500 && !job.metadata.hasStripeData) {
|
||||
const result = await this.perform(job.id);
|
||||
const importLabelModel = result.imported ? await LabelModel.findOne(importLabel) : null;
|
||||
return {
|
||||
meta: {
|
||||
stats: {
|
||||
imported: result.imported,
|
||||
invalid: result.errors
|
||||
},
|
||||
import_label: importLabelModel
|
||||
}
|
||||
};
|
||||
} else {
|
||||
const emailRecipient = user.email;
|
||||
this._addJob({
|
||||
job: async () => {
|
||||
const result = await this.perform(job.id);
|
||||
const importLabelModel = result.imported ? await LabelModel.findOne(importLabel) : null;
|
||||
const emailContent = this.generateCompletionEmail(result, {
|
||||
emailRecipient,
|
||||
importLabel: importLabelModel ? importLabelModel.toJSON() : null
|
||||
});
|
||||
const errorCSV = this.generateErrorCSV(result);
|
||||
const emailSubject = result.imported > 0 ? 'Your member import is complete' : 'Your member import was unsuccessful';
|
||||
|
||||
await this._sendEmail({
|
||||
to: emailRecipient,
|
||||
subject: emailSubject,
|
||||
html: emailContent,
|
||||
forceTextContent: true,
|
||||
attachments: [{
|
||||
filename: `${importLabel.name} - Errors.csv`,
|
||||
contents: errorCSV,
|
||||
contentType: 'text/csv',
|
||||
contentDisposition: 'attachment'
|
||||
}]
|
||||
});
|
||||
},
|
||||
offloaded: false
|
||||
});
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
};
|
|
@ -1 +0,0 @@
|
|||
module.exports = require('./importer');
|
|
@ -3,7 +3,7 @@ const tpl = require('@tryghost/tpl');
|
|||
const MembersSSR = require('@tryghost/members-ssr');
|
||||
const db = require('../../data/db');
|
||||
const MembersConfigProvider = require('./config');
|
||||
const MembersCSVImporter = require('./importer');
|
||||
const MembersCSVImporter = require('@tryghost/members-importer');
|
||||
const MembersStats = require('./stats');
|
||||
const createMembersApiInstance = require('./api');
|
||||
const createMembersSettingsInstance = require('./settings');
|
||||
|
|
|
@ -73,6 +73,7 @@
|
|||
"@tryghost/magic-link": "1.0.7",
|
||||
"@tryghost/members-api": "1.22.1",
|
||||
"@tryghost/members-csv": "1.1.2",
|
||||
"@tryghost/members-importer": "0.1.0",
|
||||
"@tryghost/members-ssr": "1.0.8",
|
||||
"@tryghost/mw-session-from-token": "0.1.22",
|
||||
"@tryghost/package-json": "1.0.2",
|
||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -845,7 +845,7 @@
|
|||
node-jose "^2.0.0"
|
||||
stripe "^8.142.0"
|
||||
|
||||
"@tryghost/members-csv@1.1.2":
|
||||
"@tryghost/members-csv@1.1.2", "@tryghost/members-csv@^1.1.2":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/members-csv/-/members-csv-1.1.2.tgz#408973028d587925ddebaf686f4b8479e2843462"
|
||||
integrity sha512-wUgKjC+OhUDbaejHhhLAl97LqyzDjzUc6CmbZ8zmwk0B1TaoFPuRGwMfGLyMmqp25b+9mTETrLWn/aSGa/0Uyw==
|
||||
|
@ -856,6 +856,16 @@
|
|||
papaparse "5.3.1"
|
||||
pump "^3.0.0"
|
||||
|
||||
"@tryghost/members-importer@0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/members-importer/-/members-importer-0.1.0.tgz#d36544e2917c41e7f35c66c91deff567c83da9b5"
|
||||
integrity sha512-eiruaYreKd7S7mGtLS2RzAMwq7T48bL4Pa5497bi3soCQCS0P8GCfonojwQmYPDHaF3xPN/oY+80q6HqP43+rw==
|
||||
dependencies:
|
||||
"@tryghost/errors" "^0.2.13"
|
||||
"@tryghost/members-csv" "^1.1.2"
|
||||
"@tryghost/tpl" "^0.1.3"
|
||||
moment-timezone "0.5.23"
|
||||
|
||||
"@tryghost/members-ssr@1.0.8":
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/members-ssr/-/members-ssr-1.0.8.tgz#ebf837b913fa049d6df36171206b372e1e1ebfd4"
|
||||
|
|
Loading…
Add table
Reference in a new issue