Feature/281 background removal plan #284
@@ -8,6 +8,7 @@ import { draftQueued } from '../intake/draftingWorker';
|
|||||||
import { checkCapacity, countForLinkSince, windowStart } from '../intake/capacity';
|
import { checkCapacity, countForLinkSince, windowStart } from '../intake/capacity';
|
||||||
import { alertCeilingReached, alertLinkThreshold } from '../intake/abuseAlert';
|
import { alertCeilingReached, alertLinkThreshold } from '../intake/abuseAlert';
|
||||||
import { getSettings } from '../adminSettings';
|
import { getSettings } from '../adminSettings';
|
||||||
|
import { isRembgConfigured } from '../intake/rembgClient';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -128,8 +129,9 @@ router.get('/:token', intakeViewLimiter, asyncRoute(async (req: Request, res: Re
|
|||||||
if (!link) {
|
if (!link) {
|
||||||
return res.status(404).json({ error: 'not found' });
|
return res.status(404).json({ error: 'not found' });
|
||||||
}
|
}
|
||||||
// The label only. Nothing about the catalogue, the admin, or other links.
|
// The label, and whether the background-removal control has anything behind
|
||||||
res.json({ label: link.label });
|
// it. Still nothing about the catalogue, the admin, or other links.
|
||||||
|
res.json({ label: link.label, backgroundRemoval: isRembgConfigured() });
|
||||||
}));
|
}));
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
@@ -159,6 +161,15 @@ router.post(
|
|||||||
|
|
||||||
const note = typeof req.body?.note === 'string' ? req.body.note.trim() : '';
|
const note = typeof req.body?.note === 'string' ? req.body.note.trim() : '';
|
||||||
|
|
||||||
|
// Absent means yes: the checkbox on the page is ticked by default, so a
|
||||||
|
// client that does not send the field — an older build, or a script — gets
|
||||||
|
// what every other submission gets rather than silently opting out.
|
||||||
|
//
|
||||||
|
// Only the exact string opts out. Multipart fields arrive as strings, and
|
||||||
|
// reading a stray value as "no" would quietly deny somebody something they
|
||||||
|
// asked for.
|
||||||
|
const removeBackground = req.body?.removeBackground !== 'false';
|
||||||
|
|
||||||
const client = await pool.connect();
|
const client = await pool.connect();
|
||||||
try {
|
try {
|
||||||
await client.query('BEGIN');
|
await client.query('BEGIN');
|
||||||
@@ -178,9 +189,9 @@ router.post(
|
|||||||
await insertItemImages(client, itemId, files, 0);
|
await insertItemImages(client, itemId, files, 0);
|
||||||
|
|
||||||
await client.query(
|
await client.query(
|
||||||
`INSERT INTO item_drafts (item_id, upload_link_id, submitter_note)
|
`INSERT INTO item_drafts (item_id, upload_link_id, submitter_note, remove_background)
|
||||||
VALUES ($1, $2, $3)`,
|
VALUES ($1, $2, $3, $4)`,
|
||||||
[itemId, link.id, note === '' ? null : note]
|
[itemId, link.id, note === '' ? null : note, removeBackground]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Counted inside the transaction and guarded on the same conditions as
|
// Counted inside the transaction and guarded on the same conditions as
|
||||||
|
|||||||
@@ -39,6 +39,13 @@ async function issueLink(label = 'Sarah', maxSubmissions?: number | null): Promi
|
|||||||
return res.body.token as string;
|
return res.body.token as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A submission with optional extra multipart fields beside the photo. */
|
||||||
|
function postPhoto(token: string, fields: Record<string, string> = {}) {
|
||||||
|
const req = request(app).post(`/api/intake/${token}`);
|
||||||
|
for (const [name, value] of Object.entries(fields)) void req.field(name, value);
|
||||||
|
return req.attach('images', PNG, 'a.png');
|
||||||
|
}
|
||||||
|
|
||||||
describe('checking a link before showing the form', () => {
|
describe('checking a link before showing the form', () => {
|
||||||
it('names the link so the page can greet the sender', async () => {
|
it('names the link so the page can greet the sender', async () => {
|
||||||
const token = await issueLink('Sarah');
|
const token = await issueLink('Sarah');
|
||||||
@@ -207,3 +214,62 @@ describe('a submitted item does not reach the storefront', () => {
|
|||||||
expect(res.body).toHaveLength(0);
|
expect(res.body).toHaveLength(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('the background-removal intent', () => {
|
||||||
|
// Ticked by default on the page, so absent means yes. An older client or a
|
||||||
|
// curl call then behaves like the current default rather than silently
|
||||||
|
// opting out of something every other submission gets.
|
||||||
|
it('defaults to true when the field is not sent', async () => {
|
||||||
|
const token = await issueLink();
|
||||||
|
await postPhoto(token);
|
||||||
|
|
||||||
|
const { rows } = await pool.query<{ remove_background: boolean }>(
|
||||||
|
`SELECT remove_background FROM item_drafts ORDER BY id DESC LIMIT 1`
|
||||||
|
);
|
||||||
|
expect(rows[0]?.remove_background).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('records a submitter who unticked it', async () => {
|
||||||
|
const token = await issueLink();
|
||||||
|
await postPhoto(token, { removeBackground: 'false' });
|
||||||
|
|
||||||
|
const { rows } = await pool.query<{ remove_background: boolean }>(
|
||||||
|
`SELECT remove_background FROM item_drafts ORDER BY id DESC LIMIT 1`
|
||||||
|
);
|
||||||
|
expect(rows[0]?.remove_background).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only the exact string opts out. A stray value is not a considered "no",
|
||||||
|
// and reading it as one would quietly deny somebody something they asked for.
|
||||||
|
it('treats anything other than "false" as consent', async () => {
|
||||||
|
const token = await issueLink();
|
||||||
|
await postPhoto(token, { removeBackground: 'no' });
|
||||||
|
|
||||||
|
const { rows } = await pool.query<{ remove_background: boolean }>(
|
||||||
|
`SELECT remove_background FROM item_drafts ORDER BY id DESC LIMIT 1`
|
||||||
|
);
|
||||||
|
expect(rows[0]?.remove_background).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('what the submission page is told', () => {
|
||||||
|
it('says the feature is off when there is no sidecar', async () => {
|
||||||
|
delete process.env.REMBG_URL;
|
||||||
|
const token = await issueLink();
|
||||||
|
|
||||||
|
const res = await request(app).get(`/api/intake/${token}`);
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body.backgroundRemoval).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('says it is on when there is one', async () => {
|
||||||
|
process.env.REMBG_URL = 'http://rembg-syn:7000';
|
||||||
|
const token = await issueLink();
|
||||||
|
|
||||||
|
const res = await request(app).get(`/api/intake/${token}`);
|
||||||
|
|
||||||
|
expect(res.body.backgroundRemoval).toBe(true);
|
||||||
|
delete process.env.REMBG_URL;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user