Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
56fdf60
wip(#510): import errors, labs lookup by file_id, source fetch/parse
lgelauff Oct 5, 2026
399c528
wip(#510): import_check module (classify, same-name keys, check files)
lgelauff Oct 5, 2026
0befcf1
wip(#510): check/download endpoints, import only by check token
lgelauff Oct 5, 2026
fbc1255
wip(#510): endpoint tests (check, download, token import, retry, expiry)
lgelauff Oct 5, 2026
8046d37
wip(#510): round form checks the source; round created once
lgelauff Oct 5, 2026
059a144
docs(#510): MONTAGE_IMPORT_CHECK_PATH in deployment.md
lgelauff Oct 5, 2026
32c1c00
test(#510): an empty import is not reported as all disqualified (#208)
lgelauff Oct 6, 2026
3be4816
fix(round): create a first round together with its files, or not at a…
lgelauff Oct 5, 2026
bc9b776
fix(#510): review fixes, backend (pr-check 2026-10-06)
lgelauff Oct 6, 2026
edc8dd5
docs(#510): deploy preflight, folder rules, troubleshooting, API docs
lgelauff Oct 6, 2026
3ed34f4
fix(#510): review fixes, round form (pr-check 2026-10-06)
lgelauff Oct 6, 2026
9a48b5f
fix(#510): a check is valid for 1 hour; files deleted after at most 7…
lgelauff Oct 6, 2026
e4378b1
fix(#510): file list accepts a pasted CSV and bare file_ids; clearer …
lgelauff Oct 7, 2026
4d81bff
fix(#510): same-name wording and screen-reader spacing in the check r…
lgelauff Oct 7, 2026
3d3593e
fix(#510): check result: row numbers on one line, readable on phones,…
lgelauff Oct 7, 2026
ec5000d
fix(#510): a bare number that is a page ID or revision ID blocks, wit…
lgelauff Oct 7, 2026
f637b7f
Merge remote-tracking branch 'origin/master' into feat/import-check-510
lgelauff Oct 7, 2026
aaf5d3b
fix(#510): import check UI review: translated reasons, clearer texts,…
lgelauff Oct 8, 2026
a074aac
fix(#510): a name taken by another file in Montage is caught by the c…
lgelauff Oct 10, 2026
dcc90b1
fix(#510): a number is a file ID; page/revision IDs only explain errors
lgelauff Oct 10, 2026
1206105
Merge remote-tracking branch 'origin/master' into feat/import-check-510
lgelauff Oct 10, 2026
4f12cf2
fix(#510): warnings-only check names what to look at, incl. found by …
lgelauff Oct 10, 2026
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
58 changes: 57 additions & 1 deletion deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,26 @@ toolforge envvars create MONTAGE_DB_URL # mysql+pymysql://<user>:<pass>@
toolforge envvars create MONTAGE_SUPERUSERS # OPTIONAL — enables su-impersonation only, NOT admin access (see "Maintainer vs superuser" below)
toolforge envvars create MONTAGE_API_LOG_PATH # e.g. /data/project/montage-beta/montage_api.log
toolforge envvars create MONTAGE_REPLAY_LOG_PATH
toolforge envvars create MONTAGE_IMPORT_CHECK_PATH # the tool's own folder, e.g. /data/project/montage-beta/import_checks (see below)
```

`MONTAGE_IMPORT_CHECK_PATH` is required everywhere except local development: Montage refuses to
start without it. It is the folder for import checks (hatnote/montage#510): each check of a
first-round import source is written there as a JSON file (mode 0600). A check can be used to
save a round for 1 hour. The file is deleted after at most 7 days: every check, import, download
and app start deletes older files, and at most 5,000 are kept. The files hold the source link,
the rows and Commons' data per file, including Commons uploader names.

- It must be a folder **of its own**: Montage refuses to start if it holds other files, because
the cleanup deletes old check files there.
- It must be absolute and on the tool's NFS share (`/data/project/<tool>/...`, mounted with
`--mount all`), so that every pod and a restarted pod see the same files.

Create the folder first, in the tool's home (`$HOME` is `/data/project/<tool>`), and print the
value to enter at the `envvars create` prompt:

```bash
mkdir -m 700 "$HOME/import_checks" && ls -ld "$HOME/import_checks" && echo "$HOME/import_checks"
```

Optional env vars (all have sensible defaults):
Expand Down Expand Up @@ -191,7 +211,25 @@ git -C ~/www/python/src checkout master
git -C ~/www/python/src pull --ff-only
```

#### 4. Run the deploy script
#### 4. Check new required settings

A release can add a required environment variable; the app then refuses to start until it is
set. Since hatnote/montage#510 that is `MONTAGE_IMPORT_CHECK_PATH` (see Fresh install, step 3).
`deploy.sh` checks the required variables before it builds, and stops if one is missing.
Before the first deploy that includes it, create the folder and set the variable on that tool
(as the tool, after `become`):

```bash
mkdir -m 700 "$HOME/import_checks" && ls -ld "$HOME/import_checks" && echo "$HOME/import_checks"
```

```bash
toolforge envvars create MONTAGE_IMPORT_CHECK_PATH
```

Enter the path the first command printed (e.g. `/data/project/montage-beta/import_checks`).

#### 5. Run the deploy script

Which ref to deploy depends on the tool (see [Multi-environment setup](#multi-environment-setup)):

Expand Down Expand Up @@ -233,6 +271,24 @@ from `toolforge envvars`. A tool that worked on the legacy service can therefore
buildservice if the `MONTAGE_*` env vars (Fresh install step 3) are not set — and its database may
need migrating (see "Startup crash: missing column" below).

#### Import checks: troubleshooting and rollback

- **The pod does not start and its log says `MONTAGE_IMPORT_CHECK_PATH ...`:** create the folder
and the variable (step 4), then `toolforge webservice buildservice restart --mount all`. No
rebuild and no rollback are needed.
- **A check of a large source fails with a server error after about 30 s:** the request hit gunicorn's
worker timeout. Nothing was saved; try a smaller source (split a list, or a sub-category).
- **Old check files:** every use of the folder (and every app start) deletes files older than
7 days. To delete them by hand:

```bash
find "$HOME/import_checks" -name '*.json' -mtime +7 -delete
```

- **Rolling back to a version before #510:** the variable and the folder can stay (older
versions ignore them). Delete the check files (`rm "$HOME"/import_checks/*.json`): they hold
Commons uploader names that nothing reads any more. Entries already imported stay.

---

## Debugging
Expand Down
85 changes: 83 additions & 2 deletions frontend/cypress/e2e/campaign.cy.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,39 @@
// #510: a clean check of one file, valid from now on
const stubCheck = () => {
const now = Date.now()
cy.intercept('POST', '/v1/admin/campaign/*/import/check', {
body: {
status: 'success',
data: {
token: 'cypress-check-token-000000000000',
checked_at: new Date(now).toISOString().replace(/\.\d+Z$/, 'Z'),
expires_at: new Date(now + 3600 * 1000).toISOString().replace(/\.\d+Z$/, 'Z'),
import_method: 'selected',
source: { file_names: ['Example.jpg'] },
columns: { name: null, file_id: null, ignored: [] },
counts: {
ok: 1, renamed: 0, duplicate: 0, unknown_name: 0,
unknown_file_id: 0, malformed_file_id: 0, same_name: 0
},
blocking: false,
total_rows: 1,
importable_count: 1,
issues: [],
issues_total: 0,
issues_truncated: false,
same_name_groups: []
}
}
}).as('checkImport')
}

const savedRound = () => ({
id: 999,
name: 'My Test Round',
import: { new_round_entry_count: 1, warnings: [], disqualified: [] }
})


describe('Campaign Details Page', () => {
beforeEach(() => {
cy.setCookie('clastic_cookie', '<cookie-validation-string>');
Expand Down Expand Up @@ -84,8 +120,53 @@ describe('Campaign Details Page', () => {
.find('li')
.first()
.click();
cy.get('.button-group button').contains('Add Round').click().click();
cy.log(' Round created successfully');
// #510 / #447: the source is checked first; Save sends the round together
// with its checked import, once
stubCheck()
cy.intercept('POST', '/v1/admin/campaign/*/add_round', (req) => {
req.reply({ delay: 500, body: { status: 'success', data: savedRound() } })
}).as('addRound')
cy.get('.form-container').contains('label', 'File List').click()
cy.get('.form-container textarea').first().type('Example.jpg')
cy.get('[data-testid="add-round-button"]').should('be.disabled')
cy.get('[data-testid="check-source-button"]').click()
cy.wait('@checkImport')
cy.get('[data-testid="import-check-result"]').should('be.visible')
// a double click must not save twice
cy.get('[data-testid="add-round-button"]').click().click({ force: true })
cy.wait('@addRound').its('request.body').should((body) => {
expect(body.import).to.deep.equal({
import_method: 'selected',
check_token: 'cypress-check-token-000000000000'
})
expect(body).not.to.have.property('file_names')
})
cy.get('@addRound.all').should('have.length', 1)
cy.get('.juror-campaign-round-card').should('not.exist')
})

it('keeps the form open when the server refuses the import', () => {
cy.get('.add-round-button').click()
cy.get('.form-container input[type="text"]').first().clear().type('My Test Round')
cy.get('.form-container').within(() => {
cy.get('input[placeholder="YYYY-MM-DD"]').first().clear().type('2025-08-15')
})
cy.get('[data-testid="userlist-search"] input').type('AadarshM07')
cy.get('[data-testid="userlist-search"]').find('li').first().click()
stubCheck()
cy.intercept('POST', '/v1/admin/campaign/*/add_round', {
statusCode: 400,
body: { status: 'failure', error_type: 'import_check_expired', detail: 'check expired' }
}).as('addRound')
cy.get('.form-container').contains('label', 'File List').click()
cy.get('.form-container textarea').first().type('Example.jpg')
cy.get('[data-testid="check-source-button"]').click()
cy.wait('@checkImport')
cy.get('[data-testid="add-round-button"]').click()
cy.wait('@addRound')
cy.get('.juror-campaign-round-card').should('exist')
cy.get('[data-testid="import-check-result"]').should('contain', 'expired')
cy.get('[data-testid="add-round-button"]').should('be.disabled')
})


Expand Down
Loading
Loading