mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-19 00:11:49 +03:00
b9b1502772
ref https://github.com/TryGhost/Product/issues/4110 Made this change to increase clarity in data export --- <!-- Leave the line below if you'd like GitHub Copilot to generate a summary from your commit --> <!-- copilot:summary --> ### <samp>🤖 Generated by Copilot at 6c0508d</samp> Renamed a column in posts export data and updated the corresponding test case. This change makes the export data more consistent and clear for users who have different member features enabled.
283 lines
10 KiB
JavaScript
283 lines
10 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.models.Product
|
|
* @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: filter ?? 'status:published,status:sent',
|
|
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 tiers = (await this.#models.Product.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');
|
|
|
|
// Weird bookshelf thing fix
|
|
if (!email.id) {
|
|
email = null;
|
|
}
|
|
|
|
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 {
|
|
id: post.get('id'),
|
|
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, tiers) : null,
|
|
newsletter_name: 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,
|
|
signups: membersTrackSources && published ? (post.get('count__signups') ?? 0) : null,
|
|
paid_conversions: membersTrackSources && paidMembersEnabled && published ? (post.get('count__paid_conversions') ?? 0) : null,
|
|
feedback_more_like_this: feedbackEnabled ? (post.get('count__positive_feedback') ?? 0) : null,
|
|
feedback_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_name');
|
|
}
|
|
|
|
if (!membersEnabled) {
|
|
removeableColumns.push('email_recipients', 'sends', 'opens', 'clicks', 'feedback_more_like_this', 'feedback_less_like_this');
|
|
} else if (!hasNewslettersWithFeedback) {
|
|
removeableColumns.push('feedback_more_like_this', 'feedback_less_like_this');
|
|
}
|
|
|
|
if (membersEnabled && !trackClicks) {
|
|
removeableColumns.push('clicks');
|
|
}
|
|
|
|
if (membersEnabled && !trackOpens) {
|
|
removeableColumns.push('opens');
|
|
}
|
|
|
|
if (!membersTrackSources || !membersEnabled) {
|
|
removeableColumns.push('signups', 'paid_conversions');
|
|
} else if (!paidMembersEnabled) {
|
|
removeableColumns.push('paid_conversions');
|
|
}
|
|
|
|
for (const columnToRemove of removeableColumns) {
|
|
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 'Members-only';
|
|
}
|
|
|
|
if (visibility === 'paid') {
|
|
return 'Paid members-only';
|
|
}
|
|
|
|
if (visibility === 'tiers') {
|
|
const tiers = post.related('tiers');
|
|
if (tiers.length === 0) {
|
|
return 'Specific tiers: none';
|
|
}
|
|
|
|
return 'Specific tiers: ' + tiers.map(tier => tier.get('name')).join(', ');
|
|
}
|
|
|
|
return visibility;
|
|
}
|
|
|
|
/**
|
|
* @private Convert an email filter to a human readable string
|
|
* @param {string} recipientFilter
|
|
* @param {*} allLabels
|
|
* @param {*} allTiers
|
|
* @returns
|
|
*/
|
|
humanReadableEmailRecipientFilter(recipientFilter, allLabels, allTiers) {
|
|
// Examples: "label:test"; "label:test,label:batch1"; "status:-free,label:test", "all"
|
|
if (recipientFilter === 'all') {
|
|
return 'All subscribers';
|
|
}
|
|
|
|
try {
|
|
const parsed = nql(recipientFilter).parse();
|
|
const strings = this.filterToString(parsed, allLabels, allTiers);
|
|
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, allTiers) {
|
|
const strings = [];
|
|
if (filter.$and) {
|
|
// Not supported
|
|
} else if (filter.$or) {
|
|
for (const subfilter of filter.$or) {
|
|
strings.push(...this.filterToString(subfilter, allLabels, allTiers));
|
|
}
|
|
} else if (filter.yg) {
|
|
// Single filter grouped in brackets
|
|
strings.push(...this.filterToString(filter.yg, allLabels, allTiers));
|
|
} 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 === 'tier') {
|
|
if (typeof filter.tier === 'string') {
|
|
const tierSlug = filter.tier;
|
|
const tier = allTiers.find(l => l.get('slug') === tierSlug);
|
|
if (tier) {
|
|
strings.push(tier.get('name'));
|
|
} else {
|
|
strings.push(tierSlug);
|
|
}
|
|
}
|
|
}
|
|
if (key === 'status') {
|
|
if (typeof filter.status === 'string') {
|
|
if (filter.status === 'free') {
|
|
strings.push('Free subscribers');
|
|
} else if (filter.status === 'paid') {
|
|
strings.push('Paid subscribers');
|
|
} else if (filter.status === 'comped') {
|
|
strings.push('Complimentary subscribers');
|
|
}
|
|
} else {
|
|
if (filter.status.$ne === 'free') {
|
|
strings.push('Paid subscribers');
|
|
}
|
|
|
|
if (filter.status.$ne === 'paid') {
|
|
strings.push('Free subscribers');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return strings;
|
|
}
|
|
}
|
|
|
|
module.exports = PostsExporter;
|