Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/backend/factories/story_localisation_factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const StoryLocalisationFactory = factory
return {
locale: 'en',
// storyId: 1,
isPublished: true,
title: faker.lorem.sentence(),
coverImage: faker.image.url(),
description: faker.lorem.paragraph(),
Expand Down
4 changes: 4 additions & 0 deletions src/backend/models/story_localisation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export default class StoryLocalisation extends BaseModel {
@column()
declare storyId: number;

@column()
declare isPublished: boolean;

@column()
declare title: string;

Expand Down Expand Up @@ -55,6 +58,7 @@ export default class StoryLocalisation extends BaseModel {
}

export const emptyTranslation: Partial<StoryLocalisation> = {
isPublished: false,
title: '',
coverImage: '',
description: '',
Expand Down
40 changes: 29 additions & 11 deletions src/backend/services/story_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,13 @@ export class StoryService {
}

public async blockingPublishMessages(
locale: string,
story: Story,
metadata?: StoryPublishMetadata,
): Promise<string[]> {
const sourceChapters = await Chapter.query()
const localeChapters = await Chapter.query()
.where('storyId', story.id)
.where('locale', this.config.languages[0].locale);
.where('locale', locale);

const messages: string[] = [];

Expand All @@ -68,7 +69,7 @@ export class StoryService {
);

const chapterMessage = publishBlockedMessage(
sourceChapters?.length ?? 0,
localeChapters?.length ?? 0,
story.chapterLimit,
metadata?.chapterType ?? story.chapterType,
metadata?.storyType ?? story.storyType,
Expand All @@ -86,7 +87,14 @@ export class StoryService {
const collected: string[] = [];
const story = await Story.query().where('id', storyId).first();
if (!story) collected.push('Story not found.');
if (story?.isPublished) collected.push('Story is published.');

const publishedLocalisation = await StoryLocalisation.query()
.where('storyId', storyId)
.where('isPublished', true)
.first();
if (publishedLocalisation || story?.isPublished) {
collected.push('Story is published.');
}

const chapters = await Chapter.query().where('storyId', storyId);
if (chapters.length > 0) collected.push('Story has published chapters.');
Expand Down Expand Up @@ -122,8 +130,8 @@ export class StoryService {
return {
model: {
chapterLimit: null,
storyType: '',
chapterType: '',
storyType: 'Story',
chapterType: 'Chapter',
sectionType: null,
visibility: 'public',
slug: null,
Expand Down Expand Up @@ -191,7 +199,7 @@ export class StoryService {
visibility: story.visibility,
slug: story.slug,
template: story.template,
isPublished: story.isPublished,
isPublished: this.localisationIsPublished(target),
...targetFields,
resources: await resourceService.hydrate(target.resources ?? []),
},
Expand Down Expand Up @@ -252,7 +260,7 @@ export class StoryService {
sectionType: story.sectionType ?? null,
visibility: story.visibility,
resources: target.resources ?? [],
isPublished: story.isPublished,
isPublished: this.localisationIsPublished(target),
};
}

Expand Down Expand Up @@ -364,7 +372,7 @@ export class StoryService {
description: local?.description ?? source?.description ?? '',
coverImage: local?.coverImage || (source?.coverImage ?? ''),
chapterLimit: story.chapterLimit,
isPublished: story.isPublished,
isPublished: this.localisationIsPublished(local),
createdAt: story.createdAt?.toISO() ?? '',
updatedAt: story.updatedAt?.toISO() ?? '',
liveCount: index?.publishedList.length ?? 0,
Expand Down Expand Up @@ -433,12 +441,19 @@ export class StoryService {
storyType: story.storyType ?? '',
visibility: story.visibility,
schemaVersion: 1,
isPublished: story.isPublished,
isPublished: this.localisationIsPublished(localisation),
fields: this.fieldsFromTemplate(story.template),
sections: localisation?.sections ?? [],
};
}

protected localisationIsPublished(
localisation?:
Pick<StoryLocalisation, 'isPublished'> | Partial<StoryLocalisation> | null,
): boolean {
return localisation?.isPublished === true;
}

private localisationFields(local: {
title?: string | null;
coverImage?: string | null;
Expand Down Expand Up @@ -489,7 +504,10 @@ export class StoryService {

const storyModels = await Story.query()
.preload('localisations', (localisationsQuery) => {
localisationsQuery.where('locale', locale);
localisationsQuery.where('locale', locale).where('isPublished', true);
})
.whereHas('localisations', (localisationsQuery) => {
localisationsQuery.where('locale', locale).where('isPublished', true);
})
.orderBy('order', 'asc');

Expand Down
1 change: 1 addition & 0 deletions src/backend/stubs/commands/seed_stories.stub
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export default class SeedStories extends BaseCommand {

await story.related('localisations').create({
locale: 'en',
isPublished: true,
title: spec.name,
coverImage: spec.coverImage,
description: spec.description,
Expand Down
38 changes: 24 additions & 14 deletions src/backend/stubs/controllers/stories_controller.stub
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export default class StoriesController {
const local = new StoryLocalisation().fill({
storyId: story.id,
locale: locale,
isPublished: false,
title: payload.title,
coverImage: payload.coverImage,
description: payload.description,
Expand Down Expand Up @@ -126,13 +127,17 @@ export default class StoriesController {
const story = await Story.findOrFail(version.storyId);

if (payload.isPublished) {
const messages = await storyService.blockingPublishMessages(story, {
title: payload.title,
coverImage: payload.coverImage,
storyType: payload.storyType,
chapterType: payload.chapterType,
visibility: payload.visibility,
});
const messages = await storyService.blockingPublishMessages(
version.locale,
story,
{
title: payload.title,
coverImage: payload.coverImage,
storyType: payload.storyType,
chapterType: payload.chapterType,
visibility: payload.visibility,
},
);
if (messages.length > 0) {
ctx.session.flash('errorsBag', { other: messages.join(' | ') });
return ctx.response.redirect().back();
Expand Down Expand Up @@ -161,13 +166,17 @@ export default class StoriesController {
payload.isPublished = true;

const model = await Story.findOrFail(version.storyId);
const messages = await storyService.blockingPublishMessages(model, {
title: payload.title,
coverImage: payload.coverImage,
storyType: payload.storyType,
chapterType: payload.chapterType,
visibility: payload.visibility,
});
const messages = await storyService.blockingPublishMessages(
version.locale,
model,
{
title: payload.title,
coverImage: payload.coverImage,
storyType: payload.storyType,
chapterType: payload.chapterType,
visibility: payload.visibility,
},
);
if (messages.length > 0) {
ctx.session.flash('errorsBag', { other: messages.join(' | ') });
return ctx.response.redirect().back();
Expand Down Expand Up @@ -218,6 +227,7 @@ export default class StoriesController {
}

const localisationData = {
isPublished: payload.isPublished,
title: payload.title,
coverImage: payload.coverImage ?? '',
description: payload.description ?? '',
Expand Down
6 changes: 6 additions & 0 deletions src/backend/stubs/migrations/stories.stub
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ export default class extends BaseSchema {

table.integer('story_id').unsigned().references('stories.id');

table
.boolean('is_published')
.notNullable()
.defaultTo(false)
.comment('whether this language edition of the story is published');

table.string('title').notNullable();

table.string('cover_image').nullable().comment('url to the cover image');
Expand Down
130 changes: 130 additions & 0 deletions src/backend/stubs/tests/unit/story_service.stub
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
StoryLocalisationFactory,
StoryService,
emptyTranslation,
ChapterFactory,
type PostedSection,
type StorySection,
type StoryVersion,
Expand Down Expand Up @@ -150,6 +151,8 @@ test.group('Story service', (group) => {
assert.isDefined(props);
assert.equal(props!.model.template, 'course');
assert.equal(props!.model.chapterLimit, 10);
assert.equal(props!.model.storyType, 'Story');
assert.equal(props!.model.chapterType, 'Chapter');
});

test('createProps returns undefined on a translation locale', async ({ assert }) => {
Expand Down Expand Up @@ -314,6 +317,133 @@ test.group('Story service', (group) => {
assert.equal(result!.resources[1].id, first.id);
assert.equal(result!.resources[0].title, 'Second Resource');
});

test('galleryIndex uses per-locale publish status', async ({ assert }) => {
const story = await StoryFactory.with('localisations').merge({ isPublished: true }).create();
const sourceLocal = await StoryLocalisation.query()
.where('storyId', story.id)
.where('locale', sourceLocale)
.firstOrFail();
sourceLocal.isPublished = true;
await sourceLocal.save();

await StoryLocalisationFactory.merge({
storyId: story.id,
locale: translationLocale,
isPublished: false,
title: 'Historia traducida',
}).create();

const service = new StoryService(testCmsConfig);
const sourceGallery = await service.galleryIndex(createGalleryHttpContext(sourceLocale));
const translationGallery = await service.galleryIndex(
createGalleryHttpContext(translationLocale),
);

assert.isTrue(sourceGallery.find((item) => item.id === story.id)?.isPublished);
assert.isFalse(translationGallery.find((item) => item.id === story.id)?.isPublished);
});

test('editProps and buildUpdatePayload use localisation publish status', async ({
assert,
}) => {
const story = await StoryFactory.with('localisations').merge({ isPublished: true }).create();
const sourceLocal = await StoryLocalisation.query()
.where('storyId', story.id)
.where('locale', sourceLocale)
.firstOrFail();
sourceLocal.isPublished = true;
await sourceLocal.save();

await StoryLocalisationFactory.merge({
storyId: story.id,
locale: translationLocale,
isPublished: false,
title: 'Historia traducida',
coverImage: 'https://example.com/es-cover.jpg',
}).create();

const service = new StoryService(testCmsConfig);
const sourceProps = await service.editProps(
createMockHttpContext({ storyId: story.id, locale: sourceLocale }),
);
const translationProps = await service.editProps(
createMockHttpContext({ storyId: story.id, locale: translationLocale }),
);
const translationPayload = await service.buildUpdatePayload(story.id, translationLocale);

assert.isTrue(sourceProps!.model.isPublished);
assert.isFalse(translationProps!.model.isPublished);
assert.isFalse(translationPayload!.isPublished);
});

test('blockingPublishMessages counts chapters for the requested locale', async ({
assert,
}) => {
const story = await StoryFactory.with('localisations')
.merge({ chapterLimit: 1, isPublished: false })
.create();

await ChapterFactory.merge({
storyId: story.id,
locale: translationLocale,
number: 1,
apiVersion: 1,
}).create();

const service = new StoryService(testCmsConfig);
const sourceMessages = await service.blockingPublishMessages(
sourceLocale,
story,
{
title: 'Ready',
coverImage: 'https://example.com/cover.jpg',
storyType: 'Story',
chapterType: 'Chapter',
visibility: 'public',
},
);
const translationMessages = await service.blockingPublishMessages(
translationLocale,
story,
{
title: 'Listo',
coverImage: 'https://example.com/cover.jpg',
storyType: 'Story',
chapterType: 'Chapter',
visibility: 'public',
},
);

assert.isAbove(sourceMessages.length, 0);
assert.lengthOf(translationMessages, 0);
});

test('homeStories excludes stories unpublished in the requested locale', async ({
assert,
}) => {
const story = await StoryFactory.with('localisations').merge({ isPublished: true }).create();
const sourceLocal = await StoryLocalisation.query()
.where('storyId', story.id)
.where('locale', sourceLocale)
.firstOrFail();
sourceLocal.isPublished = true;
await sourceLocal.save();

await StoryLocalisationFactory.merge({
storyId: story.id,
locale: translationLocale,
isPublished: false,
title: 'Historia traducida',
}).create();

const service = new StoryService(testCmsConfig);
const { stories: sourceStories } = await service.homeStories(sourceLocale);
const { stories: translationStories } = await service.homeStories(translationLocale);

assert.isDefined(sourceStories.find((item) => item.id === story.id));
assert.isUndefined(translationStories.find((item) => item.id === story.id));
});
});

test.group('StoryService.prepSections', (group) => {
Expand Down
4 changes: 2 additions & 2 deletions src/backend/validators/story.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ const createSchema = {
description: vine.string().trim().optional(),
chapterLimit: vine.number().min(1),
tags: vine.string().optional(),
storyType: vine.string().trim().nullable().optional(),
chapterType: vine.string().trim().nullable().optional(),
storyType: vine.string().trim().minLength(1),
chapterType: vine.string().trim().minLength(1),
sectionType: vine.string().optional(),
visibility: vine.string().trim().optional(),
template: vine.string().trim().minLength(1),
Expand Down
Loading
Loading