OCR roster table: who / wiki page / what changes on that profile
OCR roster: treat MW API true as empty string; table when 24 people detected
Line 1,353: Line 1,353:
{ id: 'other', label: 'Other document' }
{ id: 'other', label: 'Other document' }
];
];
/** MediaWiki JSON encodes PHP true as "" and false as null/omitted. */
function mwApiBool(v) {
if (v === true || v === 1 || v === '1' || v === 'true') {
return true;
}
if (v === false || v === 0 || v === '0' || v === 'false' || v == null) {
return false;
}
if (v === '') {
return true;
}
return !!v;
}
function isMultiPersonJob(job) {
if (!job) {
return false;
}
if (mwApiBool(job.multi_person)) {
return true;
}
var detected = job.people_summary && job.people_summary.detected;
if (Number(detected) > 1) {
return true;
}
var keys = {};
(job.proposed || []).forEach(function (p) {
if (p.person_key) {
keys[p.person_key] = 1;
}
});
return Object.keys(keys).length > 1;
}
function factAlreadyOnPage(fact) {
return !!(fact && mwApiBool(fact.already_on_page));
}
function factSelected(fact) {
return !!(fact && mwApiBool(fact.selected));
}


function changeLineForFact(fact) {
function changeLineForFact(fact) {
Line 1,358: Line 1,400:
var val = (fact.value || '').trim();
var val = (fact.value || '').trim();
var label = (fact.label || '').toLowerCase();
var label = (fact.label || '').toLowerCase();
if (fact.already_on_page && (field === 'ipw_ge' || field === 'teams')) {
if (factAlreadyOnPage(fact) && (field === 'ipw_ge' || field === 'teams')) {
return 'Team already on their profile (' + (val || fact.existing_value || 'same') + ') — no change';
return 'Team already on their profile (' + (val || fact.existing_value || 'same') + ') — no change';
}
}
if (fact.already_on_page) {
if (factAlreadyOnPage(fact)) {
return (fact.label || 'Already on page') + ' — skipped';
return (fact.label || 'Already on page') + ' — skipped';
}
}
Line 1,449: Line 1,491:
}
}
var val = (fact.value || '').trim();
var val = (fact.value || '').trim();
if (fact.already_on_page) {
if (factAlreadyOnPage(fact)) {
return 'Their profile already lists this team — nothing to write.';
return 'Their profile already lists this team — nothing to write.';
}
}
Line 1,497: Line 1,539:
function renderChangeRow(fact, job, opts) {
function renderChangeRow(fact, job, opts) {
opts = opts || {};
opts = opts || {};
var writable = isWritableFact(fact) && !fact.already_on_page;
var writable = isWritableFact(fact) && !factAlreadyOnPage(fact);
var forceSelected = opts.forceSelected;
var forceSelected = opts.forceSelected;
var selected = writable && (forceSelected === undefined ? !!fact.selected : !!forceSelected);
var selected = writable && (forceSelected === undefined ? factSelected(fact) : !!forceSelected);
var $row = $('<div>', {
var $row = $('<div>', {
'class': 'ocr-fact-row' +
'class': 'ocr-fact-row' +
(fact.already_on_page ? ' ocr-fact-already' : '') +
(factAlreadyOnPage(fact) ? ' ocr-fact-already' : '') +
(writable ? ' ocr-fact-writable' : ' ocr-fact-note') +
(writable ? ' ocr-fact-writable' : ' ocr-fact-note') +
(isPrimaryTeamFact(fact) ? ' ocr-fact-primary' : ' ocr-fact-secondary') +
(isPrimaryTeamFact(fact) ? ' ocr-fact-primary' : ' ocr-fact-secondary') +
Line 1,535: Line 1,577:
'field ' + (fact.field || fact.kind || '') + ' · conf ' + (fact.confidence || 0) + '%'
'field ' + (fact.field || fact.kind || '') + ' · conf ' + (fact.confidence || 0) + '%'
));
));
if (writable || fact.already_on_page) {
if (writable || factAlreadyOnPage(fact)) {
$row.append($check, $line, $tgt, $details);
$row.append($check, $line, $tgt, $details);
} else {
} else {
Line 1,569: Line 1,611:
var teamFacts = items.filter(isPrimaryTeamFact);
var teamFacts = items.filter(isPrimaryTeamFact);
var secondary = items.filter(function (f) {
var secondary = items.filter(function (f) {
return isWritableFact(f) && !isPrimaryTeamFact(f) && !f.already_on_page;
return isWritableFact(f) && !isPrimaryTeamFact(f) && !factAlreadyOnPage(f);
});
});
var notes = items.filter(function (f) {
var notes = items.filter(function (f) {
Line 1,575: Line 1,617:
});
});
var teamFact = teamFacts[0];
var teamFact = teamFacts[0];
var teamDone = !!(teamFact && teamFact.already_on_page);
var teamDone = !!(teamFact && factAlreadyOnPage(teamFact));
var teamReady = !!(teamFact && !teamFact.already_on_page && (teamFact.value || '').trim());
var teamReady = !!(teamFact && !factAlreadyOnPage(teamFact) && (teamFact.value || '').trim());
var included = !!(title && teamReady && teamFact.selected !== false);
var included = !!(title && teamReady && factSelected(teamFact));
var teamLabel = (teamFact && (teamFact.value || '').trim())
var teamLabel = (teamFact && (teamFact.value || '').trim())
? ('IPW ' + String(teamFact.value).replace(/^IPW\s+/i, ''))
? ('IPW ' + String(teamFact.value).replace(/^IPW\s+/i, ''))
Line 1,838: Line 1,880:


var primary = proposed.filter(function (f) {
var primary = proposed.filter(function (f) {
return isWritableFact(f) && !f.already_on_page;
return isWritableFact(f) && !factAlreadyOnPage(f);
});
});
var skipped = proposed.filter(function (f) { return f.already_on_page; });
var skipped = proposed.filter(function (f) { return factAlreadyOnPage(f); });
var notes = proposed.filter(function (f) { return f.kind === 'note'; });
var notes = proposed.filter(function (f) { return f.kind === 'note'; });


Line 1,919: Line 1,961:
function renderReview($host, job, cfg) {
function renderReview($host, job, cfg) {
$host.empty();
$host.empty();
var $panel = $('<div>', { 'class': 'ocr-review-panel ocr-review-v2' + (job.multi_person ? ' ocr-review-roster' : '') });
var $panel = $('<div>', { 'class': 'ocr-review-panel ocr-review-v2' + (isMultiPersonJob(job) ? ' ocr-review-roster' : '') });
if (job.multi_person) {
if (isMultiPersonJob(job)) {
$panel.append($('<p>').append(
$panel.append($('<p>').append(
$('<strong>').text('Each row is one person'),
$('<strong>').text('Each row is one person'),
Line 1,934: Line 1,976:
}
}


if (job.multi_person && job.people_summary) {
if (isMultiPersonJob(job) && job.people_summary) {
var ps = job.people_summary;
var ps = job.people_summary;
var $banner = $('<div>', { 'class': 'ocr-outcome-banner' });
var $banner = $('<div>', { 'class': 'ocr-outcome-banner' });
Line 1,963: Line 2,005:
if (job.identity_warning) {
if (job.identity_warning) {
var $id = $('<div>', {
var $id = $('<div>', {
'class': 'ocr-identity-banner' + (job.identity_ok ? ' ocr-identity-ok' : ' ocr-identity-warn')
'class': 'ocr-identity-banner' + (mwApiBool(job.identity_ok) ? ' ocr-identity-ok' : ' ocr-identity-warn')
});
});
$id.append($('<strong>').text(job.identity_ok ? 'Identity check: ' : 'Identity warning: '));
$id.append($('<strong>').text(mwApiBool(job.identity_ok) ? 'Identity check: ' : 'Identity warning: '));
$id.append(document.createTextNode(job.identity_warning));
$id.append(document.createTextNode(job.identity_warning));
$panel.append($id);
$panel.append($id);
Line 2,004: Line 2,046:
$previewBox.append($('<strong>').text('Soldier pages that will be edited'));
$previewBox.append($('<strong>').text('Soldier pages that will be edited'));
$previewBox.append($('<p>', { 'class': 'ocr-fact-meta ocr-preview-hint' }).text(
$previewBox.append($('<p>', { 'class': 'ocr-fact-meta ocr-preview-hint' }).text(
job.multi_person
isMultiPersonJob(job)
? 'Grouped by wiki page. Rank/role/ASN from the scan are not listed here unless you check them under a row.'
? 'Grouped by wiki page. Rank/role/ASN from the scan are not listed here unless you check them under a row.'
: 'Checked items below will be written on Apply.'
: 'Checked items below will be written on Apply.'
Line 2,016: Line 2,058:
var byPage = {};
var byPage = {};
props.forEach(function (p) {
props.forEach(function (p) {
if (!p.selected || !isWritableFact(p) || p.already_on_page) {
if (!p.selected || !isWritableFact(p) || factAlreadyOnPage(p)) {
return;
return;
}
}
Line 2,033: Line 2,075:
$previewCount.text('');
$previewCount.text('');
$previewUl.append($('<li>', { 'class': 'ocr-fact-meta' }).text(
$previewUl.append($('<li>', { 'class': 'ocr-fact-meta' }).text(
job.multi_person
isMultiPersonJob(job)
? 'Nothing will be written on Apply. Check Write? on a row (after matching a wiki page) to add that person.'
? 'Nothing will be written on Apply. Check Write? on a row (after matching a wiki page) to add that person.'
: 'Nothing selected yet.'
: 'Nothing selected yet.'
Line 2,055: Line 2,097:


$facts.append($previewBox);
$facts.append($previewBox);
if (job.multi_person) {
if (isMultiPersonJob(job)) {
var $toolbar = $('<div>', { 'class': 'ocr-multi-toolbar' });
var $toolbar = $('<div>', { 'class': 'ocr-multi-toolbar' });
var $selMatched = $('<button>', { type: 'button', text: 'Write team for every matched person who still needs it' });
var $selMatched = $('<button>', { type: 'button', text: 'Write team for every matched person who still needs it' });
Line 2,108: Line 2,150:


var $overwrite = $('<label>').append(
var $overwrite = $('<label>').append(
$('<input>', { type: 'checkbox', checked: !!job.overwrite }),
$('<input>', { type: 'checkbox', checked: mwApiBool(job.overwrite) }),
' Overwrite existing infobox fields (default: fill empty only)'
' Overwrite existing infobox fields (default: fill empty only)'
);
);
var $gallery = $('<label>').append(
var $gallery = $('<label>').append(
$('<input>', { type: 'checkbox', checked: job.add_to_gallery === true }),
$('<input>', { type: 'checkbox', checked: mwApiBool(job.add_to_gallery) }),
' Add document to Gallery on Apply' + (job.multi_person ? ' (off by default for multi-person)' : '')
' Add document to Gallery on Apply' + (isMultiPersonJob(job) ? ' (off by default for multi-person)' : '')
);
);
var $forceId = $('<label>', { 'class': 'ocr-force-identity' }).append(
var $forceId = $('<label>', { 'class': 'ocr-force-identity' }).append(
Line 2,119: Line 2,161:
' I confirmed this document matches this soldier (override identity guard)'
' I confirmed this document matches this soldier (override identity guard)'
);
);
if (job.identity_ok || job.multi_person) {
if (mwApiBool(job.identity_ok) || isMultiPersonJob(job)) {
$forceId.hide();
$forceId.hide();
}
}
Line 2,141: Line 2,183:
type: 'button',
type: 'button',
'class': 'ocr-apply-btn',
'class': 'ocr-apply-btn',
text: job.multi_person ? 'Apply selected' : 'Apply selected'
text: isMultiPersonJob(job) ? 'Apply selected' : 'Apply selected'
});
});
var $status = $('<p>', { 'class': 'ocr-review-status' });
var $status = $('<p>', { 'class': 'ocr-review-status' });
Line 2,169: Line 2,211:
var $cb = $row.find('input[type="checkbox"]').first();
var $cb = $row.find('input[type="checkbox"]').first();
base.selected = $cb.length ? !!$cb.prop('checked') : false;
base.selected = $cb.length ? !!$cb.prop('checked') : false;
if ($row.hasClass('ocr-fact-is-note') || ($row.hasClass('ocr-fact-note') && !base.already_on_page && base.kind === 'note')) {
if ($row.hasClass('ocr-fact-is-note') || ($row.hasClass('ocr-fact-note') && !factAlreadyOnPage(base) && base.kind === 'note')) {
base.selected = false;
base.selected = false;
}
}
Line 2,262: Line 2,304:


$apply.on('click', function () {
$apply.on('click', function () {
if (!job.multi_person && !job.identity_ok && !$forceId.find('input').prop('checked')) {
if (!isMultiPersonJob(job) && !mwApiBool(job.identity_ok) && !$forceId.find('input').prop('checked')) {
$status.text('Confirm identity (Advanced → checkbox) before Apply, or fix the wrong document.');
$status.text('Confirm identity (Advanced → checkbox) before Apply, or fix the wrong document.');
$adv.prop('open', true);
$adv.prop('open', true);
Line 2,489: Line 2,531:
var $meta = $('<div>', { 'class': 'ocr-queue-meta' });
var $meta = $('<div>', { 'class': 'ocr-queue-meta' });
var ps = job.people_summary || {};
var ps = job.people_summary || {};
var peopleBit = job.multi_person
var peopleBit = isMultiPersonJob(job)
? ((ps.detected || 0) + ' people · ' + (ps.matched || 0) + ' matched · ' + (ps.unresolved || 0) + ' unmatched')
? ((ps.detected || 0) + ' people · ' + (ps.matched || 0) + ' matched · ' + (ps.unresolved || 0) + ' unmatched')
: (job.bound_title ? job.bound_title : 'single document');
: (job.bound_title ? job.bound_title : 'single document');