mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-21 01:41:46 +03:00
7c2b2d0f68
fixes https://github.com/TryGhost/Team/issues/2779 fixes https://github.com/TryGhost/Team/issues/2781 Needs some more manual testing, unit tests and E2E tests. But the exporting is implemented and some columns are removed based on the site settings.
268 lines
9.8 KiB
JavaScript
268 lines
9.8 KiB
JavaScript
const nql = require('@tryghost/nql');
|
|
const logging = require('@tryghost/logging');
|
|
|
|
class PostsExporter {
|
|
#models;
|
|
#getPostUrl;
|
|
#settingsCache;
|
|
#settingsHelpers;
|
|
|
|
/**
|
|
* @param {Object} dependencies
|
|
* @param {Object} dependencies.models
|
|
* @param {Object} dependencies.models.Post
|
|
* @param {Object} dependencies.models.Newsletter
|
|
* @param {Object} dependencies.models.Label
|
|
* @param {Object} dependencies.getPostUrl
|
|
* @param {Object} dependencies.settingsCache
|
|
* @param {Object} dependencies.settingsHelpers
|
|
*/
|
|
constructor({models, getPostUrl, settingsCache, settingsHelpers}) {
|
|
this.#models = models;
|
|
this.#getPostUrl = getPostUrl;
|
|
this.#settingsCache = settingsCache;
|
|
this.#settingsHelpers = settingsHelpers;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {object} options
|
|
* @param {string} [options.filter]
|
|
* @param {string} [options.order]
|
|
* @param {string|number} [options.limit]
|
|
*/
|
|
async export({filter, order, limit}) {
|
|
const posts = await this.#models.Post.findPage({
|
|
filter,
|
|
order,
|
|
limit,
|
|
withRelated: [
|
|
'tiers',
|
|
'tags',
|
|
'authors',
|
|
'count.signups',
|
|
'count.paid_conversions',
|
|
'count.clicks',
|
|
'count.positive_feedback',
|
|
'count.negative_feedback',
|
|
'email'
|
|
]
|
|
});
|
|
|
|
const newsletters = (await this.#models.Newsletter.findAll()).models;
|
|
const labels = (await this.#models.Label.findAll()).models;
|
|
|
|
const membersEnabled = this.#settingsHelpers.isMembersEnabled();
|
|
const membersTrackSources = membersEnabled && this.#settingsCache.get('members_track_sources');
|
|
const paidMembersEnabled = membersEnabled && this.#settingsHelpers.arePaidMembersEnabled();
|
|
const trackOpens = this.#settingsCache.get('email_track_opens');
|
|
const trackClicks = this.#settingsCache.get('email_track_clicks');
|
|
const hasNewslettersWithFeedback = !!newsletters.find(newsletter => newsletter.get('feedback_enabled'));
|
|
|
|
const mapped = posts.data.map((post) => {
|
|
let email = post.related('email');
|
|
let published = true;
|
|
if (post.get('status') === 'draft' || post.get('status') === 'scheduled') {
|
|
// Manually clear it to avoid including information for a post that was reverted to draft
|
|
email = null;
|
|
published = false;
|
|
}
|
|
|
|
const feedbackEnabled = email && email.get('feedback_enabled') && hasNewslettersWithFeedback;
|
|
const showEmailClickAnalytics = trackClicks && email && email.get('track_clicks');
|
|
|
|
return {
|
|
title: post.get('title'),
|
|
url: this.#getPostUrl(post),
|
|
author: post.related('authors').map(author => author.get('name')).join(', '),
|
|
status: this.mapPostStatus(post.get('status'), !!email),
|
|
created_at: post.get('created_at'),
|
|
updated_at: post.get('updated_at'),
|
|
published_at: published ? post.get('published_at') : null,
|
|
featured: post.get('featured'),
|
|
tags: post.related('tags').map(tag => tag.get('name')).join(', '),
|
|
post_access: this.postAccessToString(post),
|
|
email_recipients: email ? this.humanReadableEmailRecipientFilter(email?.get('recipient_filter'), labels) : null,
|
|
newsletter: newsletters.length > 1 && post.get('newsletter_id') && email ? newsletters.find(newsletter => newsletter.get('id') === post.get('newsletter_id')).get('name') : null,
|
|
sends: email?.get('email_count') ?? null,
|
|
opens: trackOpens ? (email?.get('opened_count') ?? null) : null,
|
|
clicks: showEmailClickAnalytics ? (post.get('count__clicks') ?? 0) : null,
|
|
free_signups: membersTrackSources && published ? (post.get('count__signups') ?? 0) : null,
|
|
paid_signups: membersTrackSources && paidMembersEnabled && published ? (post.get('count__paid_conversions') ?? 0) : null,
|
|
reacted_with_more_like_this: feedbackEnabled ? (post.get('count__positive_feedback') ?? 0) : null,
|
|
reacted_with_less_like_this: feedbackEnabled ? (post.get('count__negative_feedback') ?? 0) : null
|
|
};
|
|
});
|
|
|
|
if (mapped.length) {
|
|
// Limit the amount of removeable columns so the structure is consistent depending on global settings
|
|
const removeableColumns = [];
|
|
|
|
if (newsletters.length <= 1) {
|
|
removeableColumns.push('newsletter');
|
|
}
|
|
|
|
if (!membersEnabled) {
|
|
removeableColumns.push('email_recipients', 'sends', 'opens', 'clicks', 'reacted_with_more_like_this', 'reacted_with_less_like_this');
|
|
} else if (!hasNewslettersWithFeedback) {
|
|
removeableColumns.push('reacted_with_more_like_this', 'reacted_with_less_like_this');
|
|
}
|
|
|
|
if (!membersEnabled && !trackClicks) {
|
|
removeableColumns.push('clicks');
|
|
}
|
|
|
|
if (!membersEnabled && !trackOpens) {
|
|
removeableColumns.push('opens');
|
|
}
|
|
|
|
if (!membersTrackSources || !membersEnabled) {
|
|
removeableColumns.push('free_signups', 'paid_signups');
|
|
} else if (!paidMembersEnabled) {
|
|
removeableColumns.push('paid_signups');
|
|
}
|
|
|
|
// Note the strict null check: we allow columns that are all zero
|
|
const columnsToRemove = removeableColumns.filter(key => mapped.every(row => !row[key] && row[key] !== 0));
|
|
|
|
for (const columnToRemove of columnsToRemove) {
|
|
for (const row of mapped) {
|
|
delete row[columnToRemove];
|
|
}
|
|
}
|
|
}
|
|
|
|
return mapped;
|
|
}
|
|
|
|
mapPostStatus(status, hasEmail) {
|
|
if (status === 'draft') {
|
|
return 'draft';
|
|
}
|
|
|
|
if (status === 'scheduled') {
|
|
return 'scheduled';
|
|
}
|
|
|
|
if (status === 'sent') {
|
|
return 'emailed only';
|
|
}
|
|
|
|
if (status === 'published') {
|
|
if (hasEmail) {
|
|
return 'published and emailed';
|
|
}
|
|
return 'published only';
|
|
}
|
|
return status;
|
|
}
|
|
|
|
postAccessToString(post) {
|
|
const visibility = post.get('visibility');
|
|
if (visibility === 'public') {
|
|
return 'Public';
|
|
}
|
|
|
|
if (visibility === 'members') {
|
|
return 'Free members';
|
|
}
|
|
|
|
if (visibility === 'paid') {
|
|
return 'Paid members';
|
|
}
|
|
|
|
if (visibility === 'tiers') {
|
|
const tiers = post.related('tiers');
|
|
if (tiers.length === 0) {
|
|
return 'Nobody';
|
|
}
|
|
|
|
return tiers.map(tier => tier.get('name')).join(', ');
|
|
}
|
|
|
|
return visibility;
|
|
}
|
|
|
|
/**
|
|
* @private Convert an email filter to a human readable string
|
|
* @param {string} recipientFilter
|
|
* @param {*} allLabels
|
|
* @returns
|
|
*/
|
|
humanReadableEmailRecipientFilter(recipientFilter, allLabels) {
|
|
// Examples: "label:test"; "label:test,label:batch1"; "status:-free,label:test", "all"
|
|
if (recipientFilter === 'all') {
|
|
return 'all';
|
|
}
|
|
|
|
try {
|
|
const parsed = nql(recipientFilter).parse();
|
|
const strings = this.filterToString(parsed, allLabels);
|
|
return strings.join(', ');
|
|
} catch (e) {
|
|
logging.error(e);
|
|
return recipientFilter;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @private Convert an email filter to a human readable string
|
|
* @param {*} filter Parsed NQL filter
|
|
* @param {*} allLabels All available member labels
|
|
* @returns
|
|
*/
|
|
filterToString(filter, allLabels) {
|
|
if (!filter) {
|
|
return [];
|
|
}
|
|
const strings = [];
|
|
if (filter.$and) {
|
|
// Not supported
|
|
} else if (filter.$or) {
|
|
for (const subfilter of filter.$or) {
|
|
strings.push(...this.filterToString(subfilter, allLabels));
|
|
}
|
|
} else if (filter.yg) {
|
|
// Single filter grouped in brackets
|
|
strings.push(...this.filterToString(filter.yg, allLabels));
|
|
} else {
|
|
for (const key of Object.keys(filter)) {
|
|
if (key === 'label') {
|
|
if (typeof filter.label === 'string') {
|
|
const labelSlug = filter.label;
|
|
const label = allLabels.find(l => l.get('slug') === labelSlug);
|
|
if (label) {
|
|
strings.push(label.get('name'));
|
|
} else {
|
|
strings.push(labelSlug);
|
|
}
|
|
}
|
|
}
|
|
if (key === 'status') {
|
|
if (typeof filter.status === 'string') {
|
|
if (filter.status === 'free') {
|
|
strings.push('free members');
|
|
} else if (filter.status === 'paid') {
|
|
strings.push('paid members');
|
|
} else if (filter.status === 'comped') {
|
|
strings.push('comped members');
|
|
}
|
|
} else {
|
|
if (filter.status.$ne === 'free') {
|
|
strings.push('paid members');
|
|
}
|
|
|
|
if (filter.status.$ne === 'paid') {
|
|
strings.push('free members');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return strings;
|
|
}
|
|
}
|
|
|
|
module.exports = PostsExporter;
|