ghost 5.20.0 → 5.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/{tryghost-adapter-manager-5.20.0.tgz → tryghost-adapter-manager-5.22.0.tgz} +0 -0
- package/components/{tryghost-api-framework-5.20.0.tgz → tryghost-api-framework-5.22.0.tgz} +0 -0
- package/components/{tryghost-api-version-compatibility-service-5.20.0.tgz → tryghost-api-version-compatibility-service-5.22.0.tgz} +0 -0
- package/components/tryghost-audience-feedback-5.22.0.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.22.0.tgz +0 -0
- package/components/{tryghost-constants-5.20.0.tgz → tryghost-constants-5.22.0.tgz} +0 -0
- package/components/tryghost-custom-theme-settings-service-5.22.0.tgz +0 -0
- package/components/tryghost-data-generator-5.22.0.tgz +0 -0
- package/components/tryghost-domain-events-5.22.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.22.0.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.22.0.tgz +0 -0
- package/components/{tryghost-email-content-generator-5.20.0.tgz → tryghost-email-content-generator-5.22.0.tgz} +0 -0
- package/components/{tryghost-express-dynamic-redirects-5.20.0.tgz → tryghost-express-dynamic-redirects-5.22.0.tgz} +0 -0
- package/components/tryghost-extract-api-key-5.22.0.tgz +0 -0
- package/components/{tryghost-html-to-plaintext-5.20.0.tgz → tryghost-html-to-plaintext-5.22.0.tgz} +0 -0
- package/components/{tryghost-job-manager-5.20.0.tgz → tryghost-job-manager-5.22.0.tgz} +0 -0
- package/components/tryghost-link-redirects-5.22.0.tgz +0 -0
- package/components/tryghost-link-replacer-5.22.0.tgz +0 -0
- package/components/tryghost-link-tracking-5.22.0.tgz +0 -0
- package/components/{tryghost-magic-link-5.20.0.tgz → tryghost-magic-link-5.22.0.tgz} +0 -0
- package/components/tryghost-mailgun-client-5.22.0.tgz +0 -0
- package/components/tryghost-member-analytics-service-5.22.0.tgz +0 -0
- package/components/tryghost-member-attribution-5.22.0.tgz +0 -0
- package/components/tryghost-member-events-5.22.0.tgz +0 -0
- package/components/tryghost-members-analytics-ingress-5.22.0.tgz +0 -0
- package/components/tryghost-members-api-5.22.0.tgz +0 -0
- package/components/tryghost-members-csv-5.22.0.tgz +0 -0
- package/components/tryghost-members-events-service-5.22.0.tgz +0 -0
- package/components/tryghost-members-importer-5.22.0.tgz +0 -0
- package/components/{tryghost-members-offers-5.20.0.tgz → tryghost-members-offers-5.22.0.tgz} +0 -0
- package/components/tryghost-members-payments-5.22.0.tgz +0 -0
- package/components/tryghost-members-ssr-5.22.0.tgz +0 -0
- package/components/tryghost-members-stripe-service-5.22.0.tgz +0 -0
- package/components/tryghost-minifier-5.22.0.tgz +0 -0
- package/components/{tryghost-mw-api-version-mismatch-5.20.0.tgz → tryghost-mw-api-version-mismatch-5.22.0.tgz} +0 -0
- package/components/{tryghost-mw-cache-control-5.20.0.tgz → tryghost-mw-cache-control-5.22.0.tgz} +0 -0
- package/components/tryghost-mw-error-handler-5.22.0.tgz +0 -0
- package/components/{tryghost-mw-session-from-token-5.20.0.tgz → tryghost-mw-session-from-token-5.22.0.tgz} +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.22.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.22.0.tgz +0 -0
- package/components/{tryghost-oembed-service-5.20.0.tgz → tryghost-oembed-service-5.22.0.tgz} +0 -0
- package/components/tryghost-package-json-5.22.0.tgz +0 -0
- package/components/tryghost-referrers-5.22.0.tgz +0 -0
- package/components/{tryghost-security-5.20.0.tgz → tryghost-security-5.22.0.tgz} +0 -0
- package/components/{tryghost-session-service-5.20.0.tgz → tryghost-session-service-5.22.0.tgz} +0 -0
- package/components/tryghost-settings-path-manager-5.22.0.tgz +0 -0
- package/components/{tryghost-staff-service-5.20.0.tgz → tryghost-staff-service-5.22.0.tgz} +0 -0
- package/components/{tryghost-stats-service-5.20.0.tgz → tryghost-stats-service-5.22.0.tgz} +0 -0
- package/components/tryghost-tiers-5.22.0.tgz +0 -0
- package/components/tryghost-update-check-service-5.22.0.tgz +0 -0
- package/components/{tryghost-verification-trigger-5.20.0.tgz → tryghost-verification-trigger-5.22.0.tgz} +0 -0
- package/components/tryghost-version-notifications-data-service-5.22.0.tgz +0 -0
- package/core/boot.js +2 -0
- package/core/built/admin/assets/{chunk.143.d245b085ad1efed4ee76.js → chunk.143.082624e9ff0d5bda7e62.js} +5 -5
- package/core/built/admin/assets/{chunk.178.c45f56ea31775e509497.js → chunk.178.890cc3faaddd738b7f6d.js} +4 -4
- package/core/built/admin/assets/{chunk.613.c4d89dc2d28c1b20348f.js → chunk.613.695f31829550fb00d43c.js} +351 -420
- package/core/built/admin/assets/{chunk.613.c4d89dc2d28c1b20348f.js.LICENSE.txt → chunk.613.695f31829550fb00d43c.js.LICENSE.txt} +0 -0
- package/core/built/admin/assets/{ghost-07e4bbf5029630b3c8a8a50c4b9f2d9e.js → ghost-625810fcb6c7ad92f79f78dea13a5ad7.js} +308 -237
- package/core/built/admin/assets/ghost-9873519a8ad69b5b23284f0a9e050bc6.css +1 -0
- package/core/built/admin/assets/ghost-dark-190bdce42b125c3d4be930bd7599b442.css +1 -0
- package/core/built/admin/assets/{vendor-518b03b02df9a55706d150627ef1004f.js → vendor-26cca1d4d56660dc6e915a12ccc3b330.js} +1038 -1003
- package/core/built/admin/index.html +6 -6
- package/core/cli/generate-data.js +51 -0
- package/core/frontend/helpers/ghost_head.js +1 -1
- package/core/server/api/endpoints/links.js +2 -1
- package/core/server/api/endpoints/tiers-public.js +2 -14
- package/core/server/api/endpoints/tiers.js +5 -51
- package/core/server/api/endpoints/utils/serializers/input/posts.js +1 -1
- package/core/server/api/endpoints/utils/serializers/input/settings.js +1 -0
- package/core/server/api/endpoints/utils/serializers/input/tiers.js +18 -27
- package/core/server/api/endpoints/utils/serializers/output/mappers/activity-feed-events.js +42 -0
- package/core/server/api/endpoints/utils/serializers/output/mappers/posts.js +5 -4
- package/core/server/api/endpoints/utils/serializers/output/tiers.js +15 -55
- package/core/server/data/db/backup.js +17 -10
- package/core/server/data/importer/importers/data/products.js +47 -0
- package/core/server/data/migrations/versions/5.21/2022-10-24-07-23-disable-feedback-enabled.js +20 -0
- package/core/server/data/migrations/versions/5.21/2022-10-25-12-05-backfill-missed-products-columns.js +35 -0
- package/core/server/data/migrations/versions/5.21/2022-10-26-04-49-add-batch-id-members-created-events.js +7 -0
- package/core/server/data/migrations/versions/5.21/2022-10-26-04-49-add-batch-id-subscription-created-events.js +7 -0
- package/core/server/data/migrations/versions/5.21/2022-10-26-04-50-member-subscription-created-batch-id.js +72 -0
- package/core/server/data/migrations/versions/5.21/2022-10-26-09-32-add-feedback-enabled-column-to-emails.js +7 -0
- package/core/server/data/migrations/versions/5.21/2022-10-27-09-50-add-member-track-source-setting.js +8 -0
- package/core/server/data/migrations/versions/5.22/2022-10-31-12-03-backfill-new-product-columns.js +35 -0
- package/core/server/data/schema/default-settings/default-settings.json +8 -0
- package/core/server/data/schema/fixtures/fixture-manager.js +16 -14
- package/core/server/data/schema/schema.js +5 -2
- package/core/server/models/base/plugins/crud.js +12 -0
- package/core/server/models/email.js +1 -0
- package/core/server/models/member-click-event.js +13 -0
- package/core/server/models/member-created-event.js +23 -0
- package/core/server/models/member-paid-subscription-event.js +4 -0
- package/core/server/models/member.js +6 -0
- package/core/server/models/post.js +1 -1
- package/core/server/models/subscription-created-event.js +7 -0
- package/core/server/services/mega/feedback-buttons.js +87 -16
- package/core/server/services/mega/mega.js +1 -0
- package/core/server/services/mega/template.js +3 -0
- package/core/server/services/member-attribution/index.js +3 -1
- package/core/server/services/members/api.js +2 -0
- package/core/server/services/members/service.js +8 -1
- package/core/server/services/newsletters/index.js +3 -1
- package/core/server/services/newsletters/service.js +11 -1
- package/core/server/services/tiers/TierRepository.js +116 -0
- package/core/server/services/tiers/index.js +1 -0
- package/core/server/services/tiers/service.js +32 -0
- package/core/shared/config/defaults.json +1 -1
- package/core/shared/labs.js +6 -7
- package/ghost.js +1 -0
- package/package.json +115 -112
- package/yarn.lock +1170 -1097
- package/components/tryghost-audience-feedback-5.20.0.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.20.0.tgz +0 -0
- package/components/tryghost-custom-theme-settings-service-5.20.0.tgz +0 -0
- package/components/tryghost-domain-events-5.20.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.20.0.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.20.0.tgz +0 -0
- package/components/tryghost-extract-api-key-5.20.0.tgz +0 -0
- package/components/tryghost-link-redirects-5.20.0.tgz +0 -0
- package/components/tryghost-link-replacer-5.20.0.tgz +0 -0
- package/components/tryghost-link-tracking-5.20.0.tgz +0 -0
- package/components/tryghost-mailgun-client-5.20.0.tgz +0 -0
- package/components/tryghost-member-analytics-service-5.20.0.tgz +0 -0
- package/components/tryghost-member-attribution-5.20.0.tgz +0 -0
- package/components/tryghost-member-events-5.20.0.tgz +0 -0
- package/components/tryghost-members-analytics-ingress-5.20.0.tgz +0 -0
- package/components/tryghost-members-api-5.20.0.tgz +0 -0
- package/components/tryghost-members-csv-5.20.0.tgz +0 -0
- package/components/tryghost-members-events-service-5.20.0.tgz +0 -0
- package/components/tryghost-members-importer-5.20.0.tgz +0 -0
- package/components/tryghost-members-payments-5.20.0.tgz +0 -0
- package/components/tryghost-members-ssr-5.20.0.tgz +0 -0
- package/components/tryghost-members-stripe-service-5.20.0.tgz +0 -0
- package/components/tryghost-minifier-5.20.0.tgz +0 -0
- package/components/tryghost-mw-error-handler-5.20.0.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.20.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.20.0.tgz +0 -0
- package/components/tryghost-package-json-5.20.0.tgz +0 -0
- package/components/tryghost-referrers-5.20.0.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.20.0.tgz +0 -0
- package/components/tryghost-tiers-5.20.0.tgz +0 -0
- package/components/tryghost-update-check-service-5.20.0.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.20.0.tgz +0 -0
- package/core/built/admin/assets/ghost-dark-363185f15c782b4b8394c5db23984e7f.css +0 -1
- package/core/built/admin/assets/ghost-fd0480352bf27e013b2b00a1bf9ffe84.css +0 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const logging = require('@tryghost/logging');
|
|
2
|
+
const {createTransactionalMigration} = require('../../utils');
|
|
3
|
+
|
|
4
|
+
module.exports = createTransactionalMigration(
|
|
5
|
+
async function up(knex) {
|
|
6
|
+
logging.info(`Fixing currency/monthly_price/yearly_price values for default paid tiers`);
|
|
7
|
+
|
|
8
|
+
const currencyUpdated = await knex('products')
|
|
9
|
+
.update('currency', 'usd')
|
|
10
|
+
.where({
|
|
11
|
+
currency: null,
|
|
12
|
+
type: 'paid'
|
|
13
|
+
});
|
|
14
|
+
logging.info(`Updated ${currencyUpdated} tier(s) where currency=null, type=paid to currency=USD`);
|
|
15
|
+
|
|
16
|
+
const monthlyPriceUpdated = await knex('products')
|
|
17
|
+
.update('monthly_price', 500)
|
|
18
|
+
.where({
|
|
19
|
+
monthly_price: null,
|
|
20
|
+
type: 'paid'
|
|
21
|
+
});
|
|
22
|
+
logging.info(`Updated ${monthlyPriceUpdated} tier(s) where monthly_price=null, type=paid to monthly_price=500`);
|
|
23
|
+
|
|
24
|
+
const yearlyPriceUpdated = await knex('products')
|
|
25
|
+
.update('yearly_price', 5000)
|
|
26
|
+
.where({
|
|
27
|
+
yearly_price: null,
|
|
28
|
+
type: 'paid'
|
|
29
|
+
});
|
|
30
|
+
logging.info(`Updated ${yearlyPriceUpdated} tier(s) where yearly_price=null, type=paid to yearly_price=5000`);
|
|
31
|
+
},
|
|
32
|
+
async function down(/* knex */) {
|
|
33
|
+
// no-op: we don't want to revert to bad data
|
|
34
|
+
}
|
|
35
|
+
);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const _ = require('lodash');
|
|
2
|
+
const logging = require('@tryghost/logging');
|
|
3
|
+
const ObjectId = require('bson-objectid').default;
|
|
4
|
+
const {createTransactionalMigration} = require('../../utils');
|
|
5
|
+
const DatabaseInfo = require('@tryghost/database-info');
|
|
6
|
+
|
|
7
|
+
// This migration links together members_created_events and members_subscription_created_events
|
|
8
|
+
|
|
9
|
+
module.exports = createTransactionalMigration(
|
|
10
|
+
async function up(knex) {
|
|
11
|
+
if (DatabaseInfo.isSQLite(knex)) {
|
|
12
|
+
logging.info('Skipped linking members_created_events and members_subscription_created_events on SQLite');
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// All events that happened within 15 minutes of each other will be linked
|
|
17
|
+
const rows = await knex('members_created_events as m')
|
|
18
|
+
.select('m.id as m_id', 's.id as s_id', 'm.member_id as member_id', 's.subscription_id as subscription_id')
|
|
19
|
+
.join('members_subscription_created_events AS s', 's.member_id', 'm.member_id')
|
|
20
|
+
.whereRaw('TIMESTAMPDIFF(MINUTE, s.created_at, m.created_at) between -15 and 15');
|
|
21
|
+
|
|
22
|
+
if (!rows.length) {
|
|
23
|
+
logging.info('Did not find linkable members_created_events and members_subscription_created_events');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Attach a unique id to each row
|
|
28
|
+
for (const row of rows) { // eslint-disable-line no-restricted-syntax
|
|
29
|
+
row.batch_id = ObjectId().toHexString();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Create batches (insertBatch doesn't support the onConflict option)
|
|
33
|
+
const batches = _.chunk(rows, 1000);
|
|
34
|
+
|
|
35
|
+
for (const batch of batches) { // eslint-disable-line no-restricted-syntax
|
|
36
|
+
// Update the members_created_events table using INSERT ON DUPLICATE KEY UPDATE trick
|
|
37
|
+
const response1 = await knex('members_created_events').insert(batch.map((r) => {
|
|
38
|
+
return {
|
|
39
|
+
id: r.m_id,
|
|
40
|
+
batch_id: r.batch_id,
|
|
41
|
+
member_id: r.member_id, // added to make the insert work
|
|
42
|
+
source: '', // added to make the insert work
|
|
43
|
+
created_at: knex.raw('NOW()') // added to make the insert work
|
|
44
|
+
};
|
|
45
|
+
})).onConflict('id').merge(['batch_id']);
|
|
46
|
+
|
|
47
|
+
if (response1[0] !== 0) {
|
|
48
|
+
logging.error(`Inserted ${response1[0]} members_created_events, expected 0`);
|
|
49
|
+
throw new Error('Rolling back');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const response2 = await knex('members_subscription_created_events').insert(batch.map((r) => {
|
|
53
|
+
return {
|
|
54
|
+
id: r.s_id,
|
|
55
|
+
batch_id: r.batch_id,
|
|
56
|
+
member_id: r.member_id, // added to make the insert work
|
|
57
|
+
subscription_id: r.subscription_id, // added to make the insert work
|
|
58
|
+
created_at: knex.raw('NOW()') // added to make the insert work
|
|
59
|
+
};
|
|
60
|
+
})).onConflict('id').merge(['batch_id']);
|
|
61
|
+
|
|
62
|
+
if (response2[0] !== 0) {
|
|
63
|
+
logging.error(`Inserted ${response1[0]} members_subscription_created_events, expected 0`);
|
|
64
|
+
throw new Error('Rolling back');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
logging.info(`Linked ${rows.length} members_created_events and members_subscription_created_events`);
|
|
68
|
+
},
|
|
69
|
+
async function down() {
|
|
70
|
+
// noop
|
|
71
|
+
}
|
|
72
|
+
);
|
package/core/server/data/migrations/versions/5.22/2022-10-31-12-03-backfill-new-product-columns.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const logging = require('@tryghost/logging');
|
|
2
|
+
|
|
3
|
+
const {createTransactionalMigration} = require('../../utils');
|
|
4
|
+
|
|
5
|
+
module.exports = createTransactionalMigration(
|
|
6
|
+
async function up(knex) {
|
|
7
|
+
const rows = await knex('products as t') // eslint-disable-line no-restricted-syntax
|
|
8
|
+
.select(
|
|
9
|
+
't.id as id',
|
|
10
|
+
'mp.amount as monthly_price',
|
|
11
|
+
'yp.amount as yearly_price',
|
|
12
|
+
knex.raw('coalesce(yp.currency, mp.currency) as currency')
|
|
13
|
+
)
|
|
14
|
+
.leftJoin('stripe_prices AS mp', 't.monthly_price_id', 'mp.id')
|
|
15
|
+
.leftJoin('stripe_prices AS yp', 't.yearly_price_id', 'yp.id')
|
|
16
|
+
.where({
|
|
17
|
+
't.type': 'paid',
|
|
18
|
+
't.currency': null
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (!rows.length) {
|
|
22
|
+
logging.info('Did not find any active paid Tiers');
|
|
23
|
+
return;
|
|
24
|
+
} else {
|
|
25
|
+
logging.info(`Updating ${rows.length} Tiers with price and currency information`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (const row of rows) { // eslint-disable-line no-restricted-syntax
|
|
29
|
+
await knex('products').update(row).where('id', row.id);
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
async function down() {
|
|
33
|
+
// no-op: we don't want to reintroduce the missing data
|
|
34
|
+
}
|
|
35
|
+
);
|
|
@@ -295,6 +295,14 @@
|
|
|
295
295
|
"members_yearly_price_id": {
|
|
296
296
|
"defaultValue": null,
|
|
297
297
|
"type": "string"
|
|
298
|
+
},
|
|
299
|
+
"members_track_sources": {
|
|
300
|
+
"defaultValue": "true",
|
|
301
|
+
"validations": {
|
|
302
|
+
"isEmpty": false,
|
|
303
|
+
"isIn": [["true", "false"]]
|
|
304
|
+
},
|
|
305
|
+
"type": "boolean"
|
|
298
306
|
}
|
|
299
307
|
},
|
|
300
308
|
"portal": {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const _ = require('lodash');
|
|
2
|
-
const Promise = require('bluebird');
|
|
3
2
|
const logging = require('@tryghost/logging');
|
|
4
3
|
const {sequence} = require('@tryghost/promise');
|
|
5
4
|
|
|
@@ -83,16 +82,16 @@ class FixtureManager {
|
|
|
83
82
|
const userRolesRelation = this.fixtures.relations.find(r => r.from.relation === 'roles');
|
|
84
83
|
await this.addFixturesForRelation(userRolesRelation, localOptions);
|
|
85
84
|
|
|
86
|
-
await
|
|
85
|
+
await sequence(this.fixtures.models.filter(m => !['User', 'Role'].includes(m.name)).map(model => () => {
|
|
87
86
|
logging.info('Model: ' + model.name);
|
|
88
87
|
|
|
89
88
|
return this.addFixturesForModel(model, localOptions);
|
|
90
|
-
});
|
|
89
|
+
}));
|
|
91
90
|
|
|
92
|
-
await
|
|
91
|
+
await sequence(this.fixtures.relations.filter(r => r.from.relation !== 'roles').map(relation => () => {
|
|
93
92
|
logging.info('Relation: ' + relation.from.model + ' to ' + relation.to.model);
|
|
94
93
|
return this.addFixturesForRelation(relation, localOptions);
|
|
95
|
-
});
|
|
94
|
+
}));
|
|
96
95
|
}
|
|
97
96
|
|
|
98
97
|
/*
|
|
@@ -191,12 +190,15 @@ class FixtureManager {
|
|
|
191
190
|
fetchRelationData(relation, options) {
|
|
192
191
|
const fromOptions = _.extend({}, options, {withRelated: [relation.from.relation]});
|
|
193
192
|
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
to: models[relation.to.model].findAll(options)
|
|
197
|
-
};
|
|
193
|
+
const fromRelations = models[relation.from.model].findAll(fromOptions);
|
|
194
|
+
const toRelations = models[relation.to.model].findAll(options);
|
|
198
195
|
|
|
199
|
-
return Promise.
|
|
196
|
+
return Promise.all([fromRelations, toRelations]).then(([from, to]) => {
|
|
197
|
+
return {
|
|
198
|
+
from: from,
|
|
199
|
+
to: to
|
|
200
|
+
};
|
|
201
|
+
});
|
|
200
202
|
}
|
|
201
203
|
|
|
202
204
|
/**
|
|
@@ -223,7 +225,7 @@ class FixtureManager {
|
|
|
223
225
|
});
|
|
224
226
|
}
|
|
225
227
|
|
|
226
|
-
const results = await
|
|
228
|
+
const results = await sequence(modelFixture.entries.map(entry => async () => {
|
|
227
229
|
let data = {};
|
|
228
230
|
|
|
229
231
|
// CASE: if id is specified, only query by id
|
|
@@ -243,7 +245,7 @@ class FixtureManager {
|
|
|
243
245
|
if (!found) {
|
|
244
246
|
return models[modelFixture.name].add(entry, options);
|
|
245
247
|
}
|
|
246
|
-
});
|
|
248
|
+
}));
|
|
247
249
|
|
|
248
250
|
return {expected: modelFixture.entries.length, done: _.compact(results).length};
|
|
249
251
|
}
|
|
@@ -308,12 +310,12 @@ class FixtureManager {
|
|
|
308
310
|
}
|
|
309
311
|
|
|
310
312
|
async removeFixturesForModel(modelFixture, options) {
|
|
311
|
-
const results = await
|
|
313
|
+
const results = await sequence(modelFixture.entries.map(entry => async () => {
|
|
312
314
|
const found = models[modelFixture.name].findOne(entry.id ? {id: entry.id} : entry, options);
|
|
313
315
|
if (found) {
|
|
314
316
|
return models[modelFixture.name].destroy(_.extend(options, {id: found.id}));
|
|
315
317
|
}
|
|
316
|
-
});
|
|
318
|
+
}));
|
|
317
319
|
|
|
318
320
|
return {expected: modelFixture.entries.length, done: results.length};
|
|
319
321
|
}
|
|
@@ -517,7 +517,8 @@ module.exports = {
|
|
|
517
517
|
type: 'string', maxlength: 50, nullable: false, validations: {
|
|
518
518
|
isIn: [['member', 'import', 'system', 'api', 'admin']]
|
|
519
519
|
}
|
|
520
|
-
}
|
|
520
|
+
},
|
|
521
|
+
batch_id: {type: 'string', maxlength: 24, nullable: true}
|
|
521
522
|
},
|
|
522
523
|
members_cancel_events: {
|
|
523
524
|
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
@@ -697,7 +698,8 @@ module.exports = {
|
|
|
697
698
|
attribution_url: {type: 'string', maxlength: 2000, nullable: true},
|
|
698
699
|
referrer_source: {type: 'string', maxlength: 191, nullable: true},
|
|
699
700
|
referrer_medium: {type: 'string', maxlength: 191, nullable: true},
|
|
700
|
-
referrer_url: {type: 'string', maxlength: 2000, nullable: true}
|
|
701
|
+
referrer_url: {type: 'string', maxlength: 2000, nullable: true},
|
|
702
|
+
batch_id: {type: 'string', maxlength: 24, nullable: true}
|
|
701
703
|
},
|
|
702
704
|
offer_redemptions: {
|
|
703
705
|
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
@@ -783,6 +785,7 @@ module.exports = {
|
|
|
783
785
|
plaintext: {type: 'text', maxlength: 1000000000, fieldtype: 'long', nullable: true},
|
|
784
786
|
track_opens: {type: 'boolean', nullable: false, defaultTo: false},
|
|
785
787
|
track_clicks: {type: 'boolean', nullable: false, defaultTo: false},
|
|
788
|
+
feedback_enabled: {type: 'boolean', nullable: false, defaultTo: false},
|
|
786
789
|
submitted_at: {type: 'dateTime', nullable: false},
|
|
787
790
|
newsletter_id: {type: 'string', maxlength: 24, nullable: true, references: 'newsletters.id'},
|
|
788
791
|
created_at: {type: 'dateTime', nullable: false},
|
|
@@ -108,6 +108,18 @@ module.exports = function (Bookshelf) {
|
|
|
108
108
|
options.order = this.orderDefaultOptions();
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
if (options.selectRaw) {
|
|
112
|
+
itemCollection.query((qb) => {
|
|
113
|
+
qb.select(qb.client.raw(options.selectRaw));
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (options.whereRaw) {
|
|
118
|
+
itemCollection.query((qb) => {
|
|
119
|
+
qb.whereRaw(options.whereRaw);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
111
123
|
const response = await itemCollection.fetchPage(options);
|
|
112
124
|
// Attributes are being filtered here, so they are not leaked into calling layer
|
|
113
125
|
// where models are serialized to json and do not do more filtering.
|
|
@@ -42,6 +42,19 @@ const MemberClickEvent = ghostBookshelf.Model.extend({
|
|
|
42
42
|
|
|
43
43
|
async destroy() {
|
|
44
44
|
throw new errors.IncorrectUsageError({message: 'Cannot destroy MemberClickEvent'});
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
permittedOptions(methodName) {
|
|
48
|
+
let options = ghostBookshelf.Model.permittedOptions.call(this, methodName);
|
|
49
|
+
const validOptions = {
|
|
50
|
+
findPage: ['selectRaw', 'whereRaw']
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (validOptions[methodName]) {
|
|
54
|
+
options = options.concat(validOptions[methodName]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return options;
|
|
45
58
|
}
|
|
46
59
|
});
|
|
47
60
|
|
|
@@ -8,6 +8,13 @@ const MemberCreatedEvent = ghostBookshelf.Model.extend({
|
|
|
8
8
|
return this.belongsTo('Member', 'member_id', 'id');
|
|
9
9
|
},
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* The subscription created event that happend at the same time (if any)
|
|
13
|
+
*/
|
|
14
|
+
subscriptionCreatedEvent() {
|
|
15
|
+
return this.belongsTo('SubscriptionCreatedEvent', 'batch_id', 'batch_id');
|
|
16
|
+
},
|
|
17
|
+
|
|
11
18
|
postAttribution() {
|
|
12
19
|
return this.belongsTo('Post', 'attribution_id', 'id');
|
|
13
20
|
},
|
|
@@ -18,6 +25,22 @@ const MemberCreatedEvent = ghostBookshelf.Model.extend({
|
|
|
18
25
|
|
|
19
26
|
tagAttribution() {
|
|
20
27
|
return this.belongsTo('Tag', 'attribution_id', 'id');
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
filterRelations() {
|
|
31
|
+
return {
|
|
32
|
+
subscriptionCreatedEvent: {
|
|
33
|
+
// Mongo-knex doesn't support belongsTo relations
|
|
34
|
+
tableName: 'members_subscription_created_events',
|
|
35
|
+
tableNameAs: 'subscriptionCreatedEvent',
|
|
36
|
+
type: 'manyToMany',
|
|
37
|
+
joinTable: 'members_created_events',
|
|
38
|
+
joinFrom: 'id',
|
|
39
|
+
joinToForeign: 'batch_id',
|
|
40
|
+
joinTo: 'batch_id',
|
|
41
|
+
joinType: 'leftJoin'
|
|
42
|
+
}
|
|
43
|
+
};
|
|
21
44
|
}
|
|
22
45
|
}, {
|
|
23
46
|
async edit() {
|
|
@@ -8,6 +8,10 @@ const MemberPaidSubscriptionEvent = ghostBookshelf.Model.extend({
|
|
|
8
8
|
return this.belongsTo('Member', 'member_id', 'id');
|
|
9
9
|
},
|
|
10
10
|
|
|
11
|
+
stripeSubscription() {
|
|
12
|
+
return this.belongsTo('StripeCustomerSubscription', 'subscription_id', 'id');
|
|
13
|
+
},
|
|
14
|
+
|
|
11
15
|
subscriptionCreatedEvent() {
|
|
12
16
|
return this.belongsTo('SubscriptionCreatedEvent', 'subscription_id', 'subscription_id');
|
|
13
17
|
},
|
|
@@ -114,6 +114,12 @@ const Member = ghostBookshelf.Model.extend({
|
|
|
114
114
|
joinTable: 'email_recipients',
|
|
115
115
|
joinFrom: 'member_id',
|
|
116
116
|
joinTo: 'email_id'
|
|
117
|
+
},
|
|
118
|
+
feedback: {
|
|
119
|
+
tableName: 'members_feedback',
|
|
120
|
+
tableNameAs: 'feedback',
|
|
121
|
+
type: 'oneToOne',
|
|
122
|
+
joinFrom: 'member_id'
|
|
117
123
|
}
|
|
118
124
|
};
|
|
119
125
|
},
|
|
@@ -1399,7 +1399,7 @@ Post = ghostBookshelf.Model.extend({
|
|
|
1399
1399
|
qb.count('*')
|
|
1400
1400
|
.from('members_feedback')
|
|
1401
1401
|
.whereRaw('posts.id = members_feedback.post_id AND members_feedback.score = 0')
|
|
1402
|
-
.as('
|
|
1402
|
+
.as('count__negative_feedback');
|
|
1403
1403
|
});
|
|
1404
1404
|
},
|
|
1405
1405
|
positive_feedback(modelOrCollection) {
|
|
@@ -8,6 +8,13 @@ const SubscriptionCreatedEvent = ghostBookshelf.Model.extend({
|
|
|
8
8
|
return this.belongsTo('Member', 'member_id', 'id');
|
|
9
9
|
},
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* The member created event that happend at the same time (if any)
|
|
13
|
+
*/
|
|
14
|
+
memberCreatedEvent() {
|
|
15
|
+
return this.belongsTo('MemberCreatedEvent', 'batch_id', 'batch_id');
|
|
16
|
+
},
|
|
17
|
+
|
|
11
18
|
subscription() {
|
|
12
19
|
return this.belongsTo('StripeCustomerSubscription', 'subscription_id', 'id');
|
|
13
20
|
},
|
|
@@ -18,20 +18,32 @@ const generateLinks = (postId, uuid, html) => {
|
|
|
18
18
|
0
|
|
19
19
|
);
|
|
20
20
|
|
|
21
|
-
html = html.replace(templateStrings.like, positiveLink.href);
|
|
22
|
-
html = html.replace(templateStrings.dislike, negativeLink.href);
|
|
21
|
+
html = html.replace(new RegExp(templateStrings.like, 'g'), positiveLink.href);
|
|
22
|
+
html = html.replace(new RegExp(templateStrings.dislike, 'g'), negativeLink.href);
|
|
23
23
|
|
|
24
24
|
return html;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
const getTemplate = (accentColor) => {
|
|
28
|
-
const likeButtonHtml = getButtonHtml(
|
|
29
|
-
|
|
28
|
+
const likeButtonHtml = getButtonHtml(
|
|
29
|
+
templateStrings.like,
|
|
30
|
+
'More like this',
|
|
31
|
+
accentColor,
|
|
32
|
+
'like-icon',
|
|
33
|
+
'https://static.ghost.org/v5.0.0/images/thumbs-up.png'
|
|
34
|
+
);
|
|
35
|
+
const dislikeButtonHtml = getButtonHtml(
|
|
36
|
+
templateStrings.dislike,
|
|
37
|
+
'Less like this',
|
|
38
|
+
accentColor,
|
|
39
|
+
'dislike-icon',
|
|
40
|
+
'https://static.ghost.org/v5.0.0/images/thumbs-down.png'
|
|
41
|
+
);
|
|
30
42
|
|
|
31
43
|
return (`
|
|
32
44
|
<tr>
|
|
33
45
|
<td dir="ltr" width="100%" style="background-color: #ffffff; text-align: center; padding: 40px 4px; border-bottom: 1px solid #e5eff5" align="center">
|
|
34
|
-
<h3 style="text-align: center; margin-bottom: 22px; font-size: 17px; letter-spacing: -0.2px; margin-top: 0 !important;">
|
|
46
|
+
<h3 style="text-align: center; margin-bottom: 22px; font-size: 17px; letter-spacing: -0.2px; margin-top: 0 !important;">Give feedback on this post</h3>
|
|
35
47
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" style="margin: auto; width: auto !important;">
|
|
36
48
|
<tr>
|
|
37
49
|
${likeButtonHtml}
|
|
@@ -43,19 +55,44 @@ const getTemplate = (accentColor) => {
|
|
|
43
55
|
`);
|
|
44
56
|
};
|
|
45
57
|
|
|
46
|
-
function getButtonHtml(href, buttonText, accentColor) {
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const textColor = color.darken(0.6).hex();
|
|
58
|
+
function getButtonHtml(href, buttonText, accentColor, className, iconUrl) {
|
|
59
|
+
const bgColor = getButtonLightTheme(accentColor).backgroundColor;
|
|
60
|
+
const textColor = getButtonLightTheme(accentColor).color;
|
|
50
61
|
|
|
51
62
|
return (`
|
|
52
|
-
|
|
53
|
-
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="width: auto
|
|
63
|
+
<td dir="ltr" valign="top" align="center" style="vertical-align: top; color: ${textColor}; font-family: inherit; font-size: 14px; text-align: center; padding: 0 8px;" nowrap>
|
|
64
|
+
<table class="feedback-buttons" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="background-color: ${bgColor}; overflow: hidden; border-radius: 22px;border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
|
|
54
65
|
<tr>
|
|
55
|
-
<td
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
66
|
+
<td width="16" height="38" style="paddig-left:10px;"></td>
|
|
67
|
+
<td class=${className} background=${iconUrl} bgcolor="${textColor}" width="24" height="38" valign="top" style="background-image: url(${iconUrl});vertical-align: middle; text-align: center;background-size: cover; background-position: 0 50%; background-repeat:no-repeat;">
|
|
68
|
+
<!--[if gte mso 9]>
|
|
69
|
+
<v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="true" stroke="false" style="width:24px;height:38px;">
|
|
70
|
+
<v:fill origin="0.5, 0.5" position="0.5, 0.5" type="tile" src=${iconUrl} color="${textColor}" size="1,1" aspect="atleast" />
|
|
71
|
+
<v:textbox inset="0,0,0,0">
|
|
72
|
+
<![endif]-->
|
|
73
|
+
<div>
|
|
74
|
+
<a style="background-color: ${bgColor};border: none; width: 24px; height: 38px; display: block" href=${href} target="_blank"></a>
|
|
75
|
+
</div>
|
|
76
|
+
<!--[if gte mso 9]>
|
|
77
|
+
</v:textbox>
|
|
78
|
+
</v:rect>
|
|
79
|
+
<![endif]-->
|
|
80
|
+
</td>
|
|
81
|
+
<td style="text-align: right;font-size: 18px; vertical-align: middle; color: ${textColor}!important; background-position: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';">
|
|
82
|
+
<div style="color: ${textColor}"><!--[if mso]>
|
|
83
|
+
<v:rect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href=${href} style="height:38px;v-text-anchor:middle;width:120px;" stroke="f">
|
|
84
|
+
<w:anchorlock/>
|
|
85
|
+
<center>
|
|
86
|
+
<![endif]-->
|
|
87
|
+
<a
|
|
88
|
+
href=${href}
|
|
89
|
+
target="_blank"
|
|
90
|
+
style="padding: 0 8px 0 8px;border-radius: 0 22px 22px 0;color:${textColor}!important;display:inline-block;font-family: inherit;font-size:14px;font-weight:bold;line-height:38px;text-align:left;text-decoration:none;width:100px;-webkit-text-size-adjust:none;">
|
|
91
|
+
${buttonText}</a>
|
|
92
|
+
<!--[if mso]>
|
|
93
|
+
</center>
|
|
94
|
+
</v:rect>
|
|
95
|
+
<![endif]--></div>
|
|
59
96
|
</td>
|
|
60
97
|
</tr>
|
|
61
98
|
</table>
|
|
@@ -63,7 +100,41 @@ function getButtonHtml(href, buttonText, accentColor) {
|
|
|
63
100
|
`);
|
|
64
101
|
}
|
|
65
102
|
|
|
103
|
+
function getButtonLightTheme(accentColor) {
|
|
104
|
+
const color = new Color(accentColor);
|
|
105
|
+
const backgroundColor = `${accentColor}10`;
|
|
106
|
+
const textColor = color.darken(0.6).hex();
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
color: textColor,
|
|
110
|
+
backgroundColor
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function getButtonsHeadStyles() {
|
|
115
|
+
return (`
|
|
116
|
+
.like-icon {
|
|
117
|
+
mix-blend-mode: darken;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.dislike-icon {
|
|
121
|
+
mix-blend-mode: darken;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@media (prefers-color-scheme: dark) {
|
|
125
|
+
.like-icon {
|
|
126
|
+
mix-blend-mode: initial !important;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.dislike-icon {
|
|
130
|
+
mix-blend-mode: initial !important;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
`);
|
|
134
|
+
}
|
|
135
|
+
|
|
66
136
|
module.exports = {
|
|
67
137
|
generateLinks,
|
|
68
|
-
getTemplate
|
|
138
|
+
getTemplate,
|
|
139
|
+
getButtonsHeadStyles
|
|
69
140
|
};
|
|
@@ -240,6 +240,7 @@ const addEmail = async (postModel, options) => {
|
|
|
240
240
|
submitted_at: moment().toDate(),
|
|
241
241
|
track_opens: !!settingsCache.get('email_track_opens'),
|
|
242
242
|
track_clicks: !!settingsCache.get('email_track_clicks'),
|
|
243
|
+
feedback_enabled: !!newsletter.get('feedback_enabled'),
|
|
243
244
|
recipient_filter: emailRecipientFilter,
|
|
244
245
|
newsletter_id: newsletter.id
|
|
245
246
|
}, knexOptions);
|
|
@@ -37,6 +37,7 @@ module.exports = ({post, site, newsletter, templateSettings}) => {
|
|
|
37
37
|
<head>
|
|
38
38
|
<meta name="viewport" content="width=device-width" />
|
|
39
39
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
|
40
|
+
<!--[if mso]><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch><o:AllowPNG/></o:OfficeDocumentSettings></xml><![endif]-->
|
|
40
41
|
<title>${cleanPost.title}</title>
|
|
41
42
|
<style>
|
|
42
43
|
/* -------------------------------------
|
|
@@ -1161,6 +1162,8 @@ ${ templateSettings.showBadge ? `
|
|
|
1161
1162
|
}
|
|
1162
1163
|
` : ''}
|
|
1163
1164
|
|
|
1165
|
+
${iff(templateSettings.feedbackEnabled, feedbackButtons.getButtonsHeadStyles(templateSettings.accentColor), '')}
|
|
1166
|
+
|
|
1164
1167
|
</style>
|
|
1165
1168
|
</head>
|
|
1166
1169
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const urlService = require('../url');
|
|
2
2
|
const urlUtils = require('../../../shared/url-utils');
|
|
3
|
+
const settingsCache = require('../../../shared/settings-cache');
|
|
3
4
|
|
|
4
5
|
class MemberAttributionServiceWrapper {
|
|
5
6
|
init() {
|
|
@@ -38,7 +39,8 @@ class MemberAttributionServiceWrapper {
|
|
|
38
39
|
SubscriptionCreatedEvent: models.SubscriptionCreatedEvent,
|
|
39
40
|
Integration: models.Integration
|
|
40
41
|
},
|
|
41
|
-
attributionBuilder: this.attributionBuilder
|
|
42
|
+
attributionBuilder: this.attributionBuilder,
|
|
43
|
+
isTrackingEnabled: !!settingsCache.get('members_track_sources')
|
|
42
44
|
});
|
|
43
45
|
}
|
|
44
46
|
}
|
|
@@ -13,6 +13,7 @@ const SingleUseTokenProvider = require('./SingleUseTokenProvider');
|
|
|
13
13
|
const urlUtils = require('../../../shared/url-utils');
|
|
14
14
|
const labsService = require('../../../shared/labs');
|
|
15
15
|
const offersService = require('../offers');
|
|
16
|
+
const tiersService = require('../tiers');
|
|
16
17
|
const newslettersService = require('../newsletters');
|
|
17
18
|
const memberAttributionService = require('../member-attribution');
|
|
18
19
|
|
|
@@ -198,6 +199,7 @@ function createApiInstance(config) {
|
|
|
198
199
|
MemberFeedback: models.MemberFeedback
|
|
199
200
|
},
|
|
200
201
|
stripeAPIService: stripeService.api,
|
|
202
|
+
tiersService: tiersService,
|
|
201
203
|
offersAPI: offersService.api,
|
|
202
204
|
labsService: labsService,
|
|
203
205
|
newslettersService: newslettersService,
|