News: Beiträge – Titel optional, Datumsprüfung, Bildregeln

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Opus 5.5 committed 2026-09-30 16:45:42 +00:00
1 parent 434bab0b7a
commit d320730251
2 files changed
+40 -6

No files matched your search

+16 -6
View File
@@ -42,7 +42,8 @@ export function visibleUntilFor(duration, from, dateIso, now = from) {
case '3m': return addMonths(from, 3)
case 'open': return null
case 'date': {
if (typeof dateIso !== 'string' || !DATE_RE.test(dateIso) || Number.isNaN(Date.parse(dateIso))) {
if (typeof dateIso !== 'string' || !DATE_RE.test(dateIso) || Number.isNaN(Date.parse(dateIso))
|| new Date(`${dateIso}T00:00:00Z`).toISOString().slice(0, 10) !== dateIso) {
throw httpError(400, 'Bitte wähle ein gültiges Datum.', 'date')
}
const today = berlinDay(now)
@@ -69,8 +70,8 @@ export function validatePostInput(input) {
const str = (v) => (typeof v === 'string' ? v.trim() : '')
const type = src.type
if (!TYPES.includes(type)) throw httpError(400, 'Bitte wähle eine Art von Beitrag.', 'type')
// Der Titel ist optional (leer gespeichert)
const title = str(src.title)
if (!title) throw httpError(400, 'Bitte gib einen Titel ein.', 'title')
if (title.length > TITLE_MAX) throw httpError(400, `Der Titel darf höchstens ${TITLE_MAX} Zeichen lang sein.`, 'title')
const text = str(src.text)
if (text.length > TEXT_MAX) throw httpError(400, `Der Text darf höchstens ${TEXT_MAX} Zeichen lang sein.`, 'text')
@@ -111,7 +112,10 @@ async function requireImage(projectId, imageId) {
export async function createPost({ customerId, projectId, input, now = new Date() }) {
const v = validatePostInput(input)
const until = visibleUntilFor(v.duration, now, v.date, now)
if (v.imageId) await requireImage(projectId, v.imageId)
if (v.imageId) {
await requireImage(projectId, v.imageId)
await claimNewsImage(projectId, v.imageId)
}
const iso = now.toISOString()
const doc = await createDocument(config.collections.newsPosts, {
customerId, projectId,
@@ -121,7 +125,6 @@ export async function createPost({ customerId, projectId, input, now = new Date(
visibleFrom: iso, visibleUntil: until ? until.toISOString() : null,
takenDownAt: null, createdAt: iso, updatedAt: iso,
})
if (v.imageId) await claimNewsImage(projectId, v.imageId)
return doc
}
@@ -153,9 +156,16 @@ export async function updatePost({ customerId, projectId, id, input, now = new D
const v = validatePostInput(input)
// Relative Dauern zaehlen ab dem urspruenglichen Beginn, damit ein Bearbeiten nichts verlaengert
const from = new Date(post.visibleFrom)
const until = visibleUntilFor(v.duration, from, v.date, now)
// Unverändertes Datum bleibt speicherbar, auch wenn es inzwischen unter "morgen" liegt
const sameDate = v.duration === 'date' && post.duration === 'date' && DATE_RE.test(v.date || '')
&& post.visibleUntil === berlinTime(v.date, 23, 59, 59, 999).toISOString()
const until = sameDate ? new Date(post.visibleUntil) : visibleUntilFor(v.duration, from, v.date, now)
const imageId = v.imageId || null
if (imageId && imageId !== post.imageId) await requireImage(projectId, imageId)
if (imageId && imageId !== post.imageId) {
await requireImage(projectId, imageId)
const used = (await projectPosts(projectId)).some((p) => p.$id !== id && p.imageId === imageId)
if (used) throw httpError(400, 'Dieses Foto gehört schon zu einem anderen Beitrag.', 'imageId')
}
patch = {
type: v.type, title: v.title, text: v.text, paper: v.paper, duration: v.duration,
visibleUntil: until ? until.toISOString() : null, imageId,
+24
View File
@@ -49,6 +49,8 @@ test('datum_grenzen_400', () => {
assert.throws(() => np.visibleUntilFor('date', NOW, '2026-10-01'), { status: 400, field: 'date' })
assert.throws(() => np.visibleUntilFor('date', NOW, '2027-10-02'), { status: 400 })
assert.throws(() => np.visibleUntilFor('date', NOW, 'morgen'), { status: 400 })
assert.throws(() => np.visibleUntilFor('date', NOW, '2026-11-31'), { status: 400, field: 'date' })
assert.throws(() => np.visibleUntilFor('date', NOW, '2027-02-31'), { status: 400, field: 'date' })
assert.ok(np.visibleUntilFor('date', NOW, '2026-10-02'))
assert.ok(np.visibleUntilFor('date', NOW, '2027-10-01'))
})
@@ -58,6 +60,8 @@ test('validierung', () => {
assert.throws(() => np.validatePostInput({ ...base, type: 'photo' }), { status: 400, field: 'imageId' })
assert.throws(() => np.validatePostInput({ ...base, paper: 'lila' }), { status: 400, field: 'paper' })
assert.throws(() => np.validatePostInput({ ...base, text: '' }), { status: 400, field: 'text' })
assert.equal(np.validatePostInput({ ...base, title: undefined }).title, '')
assert.equal(np.validatePostInput({ ...base, type: 'photo', title: '', text: '', imageId: 'i1' }).title, '')
assert.throws(() => np.validatePostInput({ ...base, text: 'x'.repeat(401) }), { status: 400, field: 'text' })
assert.throws(() => np.validatePostInput({ ...base, type: 'combo', imageId: 'i1', text: '' }), { status: 400, field: 'text' })
assert.equal(np.validatePostInput({ ...base, type: 'photo', text: '', imageId: 'i1' }).imageId, 'i1')
@@ -118,3 +122,23 @@ test('bild_gehoert_zum_projekt_und_wird_mitgeloescht', async () => {
assert.equal(existsSync(newsImagePath('pd', b.imageId, 'l')), false)
assert.equal((await np.listPosts('pd', NOW)).hanging.length, 0)
})
test('bearbeiten_datum_bild_regeln', async () => {
const post = await mk('pf', { duration: 'date', date: '2026-10-10' })
const edit = (input, now) => np.updatePost({ customerId: 'c1', projectId: 'pf', id: post.$id, input: { ...base, duration: 'date', ...input }, now })
const late = new Date('2026-10-10T08:00:00.000Z')
// unveraendertes Datum (heute) bleibt speicherbar, geaendertes wird geprueft
const ok = await edit({ date: '2026-10-10', title: 'Neu' }, late)
assert.equal(ok.title, 'Neu')
assert.equal(ok.visibleUntil, post.visibleUntil)
await assert.rejects(edit({ date: '2026-10-11' }, new Date('2026-10-11T08:00:00.000Z')), { status: 400, field: 'date' })
// Bild eines anderen Beitrags
const img = await storeNewsImage({ customerId: 'c1', projectId: 'pf', buffer: await png() })
await mk('pf', { type: 'photo', imageId: img.imageId })
await assert.rejects(edit({ type: 'photo', imageId: img.imageId, date: '2026-10-10' }, late), { status: 400, field: 'imageId' })
})
test('create_ohne_bild_belegt_nichts', async () => {
await assert.rejects(mk('pg', { type: 'photo', imageId: 'nope' }), { status: 400, field: 'imageId' })
assert.equal((await np.listPosts('pg', NOW)).hanging.length, 0)
})