From 01398ffce0fd6a9f43563381d0b8d385b1e6806a Mon Sep 17 00:00:00 2001 From: ke4tch Date: Sun, 14 Dec 2025 11:29:18 -0500 Subject: [PATCH 1/7] Correct recommended tags and templates parsing. Improve source checking. Cleanup. --- src/features/bioCheck/BioCheckPerson.js | 2 + src/features/bioCheck/Biography.js | 137 +++++++++++------------- src/features/bioCheck/SourceRules.js | 35 +++++- 3 files changed, 97 insertions(+), 77 deletions(-) diff --git a/src/features/bioCheck/BioCheckPerson.js b/src/features/bioCheck/BioCheckPerson.js index 4dcbc748..aee0b22b 100644 --- a/src/features/bioCheck/BioCheckPerson.js +++ b/src/features/bioCheck/BioCheckPerson.js @@ -226,6 +226,8 @@ export class BioCheckPerson { // to see if resolveRedirect was not honored by the API // look for a bio content that starts with // and if so set hasBio false to force a call to the getBio API + // Note that this might happen for check Watchlist, since the + // API does not honor resolveRedirect as of Dec 2025 if (profileObj.bio.startsWith("#REDIRECT")) { console.log("BioCheck biography starts with #REDIRECT for profile Id " + profileObj.Id); this.person.hasBio = false; diff --git a/src/features/bioCheck/Biography.js b/src/features/bioCheck/Biography.js index 643a4ee6..ab9922a9 100644 --- a/src/features/bioCheck/Biography.js +++ b/src/features/bioCheck/Biography.js @@ -233,6 +233,7 @@ export class Biography { // so you want to combine them until you find the line with the }} // and you also want the line after the {{ and up to the first | or the }} to test while (currentIndex < lineCount) { + let mixedCaseLine = this.#bioLines[currentIndex].trim(); let line = this.#bioLines[currentIndex].toLowerCase().trim(); let linesToSkip = 0; if (line.length > 0) { // something here? @@ -248,7 +249,7 @@ export class Biography { if (this.#checkForEmail(line)) { this.#style.bioMightHaveEmail = true; } - this.#checkRecommendedHtml(line); + this.#checkRecommendedHtml(line, mixedCaseLine); if (this.#bioSearchString.length > 0) { if (line.includes(this.#bioSearchString.toLowerCase())) { @@ -263,7 +264,6 @@ export class Biography { // Report category out of order with the last thing reported first so that // you only get one reported per category // out of order if RNB, Project Box, Nav Box or Biography heading preceeds - //if (haveResearchNoteBox || haveNavBox || haveProjectBox || haveBiography || haveTextLine) { if (haveResearchNoteBox || haveNavBoxConfused || haveNavBoxSuccession || haveProjectBox || haveBiography || haveTextLine) { this.#style.bioCategoryNotAtStart = true; if (haveBiography) { @@ -291,9 +291,6 @@ export class Biography { } } this.#stats.bioHasCategories = true; - //if (this.#firstCategoryIndex < 0) { - // this.#firstCategoryIndex = currentIndex; - //} if (line.includes(Biography.#UNSOURCED)) { this.#stats.bioIsMarkedUnsourced = true; } @@ -312,16 +309,14 @@ export class Biography { } } - } else { + } else { // not a category let partialLine = ''; let partialMixedCaseLine = ''; - // TODO need to handle the case of multiple templates on the same line - // you can see this with profile Haraldsdotter-37 if (line.startsWith(Biography.#TEMPLATE_START)) { // handle case of template on multiple lines let j = line.indexOf(Biography.#TEMPLATE_END); let combinedLine = line; - let combinedLineMixedCase = line; + let combinedLineMixedCase = mixedCaseLine; let nextIndex = currentIndex + 1; let foundEnd = true; if (j < 0) { @@ -351,48 +346,7 @@ export class Biography { partialLine = line.substring(2, j).trim().toLowerCase(); partialMixedCaseLine = this.#bioLines[currentIndex].substring(2, j).trim(); - // Check that none of the template parameters are duplicated - /* - * remove any external links from the parameters (they may contain |) - * find the start of a parameter name following the | - * find the end of the parameter name before the = - * trim the parameter name * add it to the set of unique names, and complain if its already found - */ - // you can test using Vandever-161 - - combinedLine = this.#swallowLink(combinedLine); - combinedLineMixedCase = this.#swallowLink(combinedLineMixedCase); - foundEnd = false; - let paramEnd = 0; - let paramNameSet = new Set(); - while (!foundEnd) { - let paramStart = combinedLine.indexOf('|', paramEnd); - if (paramStart < 0) { - foundEnd = true; - } else { - paramStart++; - paramEnd = combinedLine.indexOf('=', paramStart); - //paramEnd--; - if (paramEnd > 0) { - let paramName=combinedLine.substring(paramStart, paramEnd).trim(); - if (paramNameSet.has(paramName)) { - let dupName = combinedLineMixedCase.substring(paramStart, paramEnd).trim(); - let msg = partialMixedCaseLine + ' template has duplicate parameter ' + dupName; - this.#messages.styleMessages.push(msg); - this.#style.bioHasStyleIssues = true; - } else { - paramNameSet.add(paramName); - } - paramEnd++; - if (paramEnd > combinedLine.length) { - foundEnd = true; - } - // check for duplicate - } else { - foundEnd = true; - } - } - } + this.#checkForDuplicateTemplateParameter(combinedLineMixedCase); } /* @@ -1024,21 +978,6 @@ export class Biography { } return outStr; } - /* - * Swallow external link in style [[ stuff here ]] - * @param {String} inStr - * @returns {String} string with link removed - */ - #swallowLink(inStr) { - // remove anything that is inside link [[ and ]] brackets - let startIndex = inStr.indexOf('[['); - let endIndex = inStr.indexOf(']]'); - if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) { - return inStr; - } - let outStr = inStr.slice(0, startIndex + 2) + inStr.slice(endIndex); - return outStr; - } /* * Build an array of each line in the bio @@ -1047,6 +986,10 @@ export class Biography { * @param {String} inStr bio string stripped of comments */ #getLines(inStr) { + // if the line has the end and start of a template, such as }}{{ then + // treat that as two separate lines + inStr = inStr.replaceAll('\}\}\{\{', '\}\}\n\{\{'); + inStr = inStr.replaceAll('\}\} \{\{', '\}\}\n\{\{'); let splitString = inStr.split("\n"); let line = ""; let tmpString = ""; @@ -1057,7 +1000,7 @@ export class Biography { // trimming tmpString = line.replace("-", " "); tmpString = tmpString.trim(); - // Do NOT ingore empty lines here. Need to check sources + // Do NOT ignore empty lines here. Need to check sources // Sanity check if the line with also has text following on same line if (tmpString.indexOf(Biography.#REFERENCES_TAG) >= 0) { let endOfReferencesTag = tmpString.indexOf(Biography.#END_BRACKET); @@ -1728,8 +1671,6 @@ export class Biography { this.#messages.styleMessages.push('Heading or subheading before Biography'); } } else { - // See https://www.wikitree.com/wiki/Help:Recommended_Tags - // this might be too aggressive if ((line.startsWith('[[')) && (line.endsWith(']]'))) { this.#unexpectedLines.push(line); } @@ -1744,10 +1685,13 @@ export class Biography { /* * Check for HTML directives that are not recommended */ - #checkRecommendedHtml(line) { - if (line.startsWith(Biography.#START_BRACKET)) { + #checkRecommendedHtml(line, mixedCaseLine) { + // some ancestry citations have 80) { msg = msg.substring(0, 80); msg += "..."; @@ -1759,6 +1703,49 @@ export class Biography { return; } + /* + * Check for duplicate template parameter + * remove any external links from the parameters (they may contain |) + * find the start of a parameter name following the | + * find the end of the parameter name before the = + * trim the parameter name * add it to the set of unique names, and complain if its already found + */ + #checkForDuplicateTemplateParameter(mixedCaseLine) { + let line = mixedCaseLine.replace(/\[\[.*?\]\]/g, ""); + let foundEnd = false; + let paramEnd = 0; + let paramNameSet = new Set(); + while (!foundEnd) { + let paramStart = line.indexOf('|', paramEnd); + if (paramStart < 0) { + foundEnd = true; + } else { + paramStart++; + paramEnd = line.indexOf('=', paramStart); + if (paramEnd > 0) { + let paramName=line.substring(paramStart, paramEnd).trim(); + let paramNameLower = paramName.toLowerCase(); + if (paramNameSet.has(paramNameLower)) { + // what we want to report is just the template name and param name + let templateName = mixedCaseLine.substring(2, mixedCaseLine.indexOf('|')); + let msg = templateName + ' template has duplicate parameter ' + paramName; + this.#messages.styleMessages.push(msg); + this.#style.bioHasStyleIssues = true; + } else { + paramNameSet.add(paramNameLower); + } + paramEnd++; + if (paramEnd > line.length) { + foundEnd = true; + } + } else { + foundEnd = true; + } + } + } + return; + } + /* ********************************************************************* * ******************* PRIVATE METHODS ********************************* * ******************* used by Validator ******************************* @@ -2380,7 +2367,7 @@ export class Biography { * * If you made a change to be able to compress multiple lines with bullets following * that would probably break finding sources that are not DNA confirmations - * see Miller-117607 TODO + * see Miller-117607 */ #isValidDnaConfirmation(line, mixedCaseLine) { let isValidConf = false; @@ -2875,9 +2862,9 @@ export class Biography { */ let linePart = line.replaceAll(';', ','); linePart = this.#sourceRules.removeInvalidSourcePart(linePart); - if (this.#tooOldToRemember) { + if ((linePart.length > 0) && (this.#tooOldToRemember)) { linePart = this.#sourceRules.removeInvalidSourcePartTooOld(linePart); - if (this.#isPre1700 || this.#treatAsPre1700) { + if ((linePart.length > 0) && (this.#isPre1700 || this.#treatAsPre1700)) { linePart = this.#sourceRules.removeInvalidSourcePartPre1700(line); } } diff --git a/src/features/bioCheck/SourceRules.js b/src/features/bioCheck/SourceRules.js index 6d1c2826..3a6715de 100644 --- a/src/features/bioCheck/SourceRules.js +++ b/src/features/bioCheck/SourceRules.js @@ -179,6 +179,7 @@ class SourceRules { "", // not recommended but used a lot " Date: Mon, 22 Dec 2025 10:56:12 -0500 Subject: [PATCH 2/7] Add bio scoring. --- src/features/bioCheck/BioCheckPerson.js | 85 +++++++- src/features/bioCheck/Biography.js | 246 ++++++++++++++++++++++-- src/features/bioCheck/README.md | 65 ++++++- src/features/bioCheck/bioCheck.js | 6 + 4 files changed, 383 insertions(+), 19 deletions(-) diff --git a/src/features/bioCheck/BioCheckPerson.js b/src/features/bioCheck/BioCheckPerson.js index aee0b22b..6bec3068 100644 --- a/src/features/bioCheck/BioCheckPerson.js +++ b/src/features/bioCheck/BioCheckPerson.js @@ -68,7 +68,13 @@ export class BioCheckPerson { isMember: false, isOrphan: false, hasLocation: false, + hasBirthLocation: false, + hasDeathLocation: false, uncheckedDueToPrivacy: false, + hasFather: false, + hasMother: false, + hasFatherStatus: false, + hasMotherStatus: false, uncheckedDueToDate: false, fatherDnaConfirmed: false, motherDnaConfirmed: false, @@ -163,17 +169,27 @@ export class BioCheckPerson { } if (profileObj.BirthLocation != null && profileObj.BirthLocation.length > 0) { this.person.hasLocation = true; + this.person.hasBirthLocation = true; } if (profileObj.DeathLocation != null && profileObj.DeathLocation.length > 0) { this.person.hasLocation = true; + this.person.hasDeathLocation = true; + } + if (profileObj.Father != null) { + this.person.hasFather = true; + } + if (profileObj.Mother != null) { + this.person.hasMother = true; } if (profileObj.DataStatus != null) { if (profileObj.DataStatus.Father != null) { + this.person.hasFatherStatus = true; if (profileObj.DataStatus.Father == BioCheckPerson.CONF_WITH_DNA_STATUS) { this.person.fatherDnaConfirmed = true; } } if (profileObj.DataStatus.Mother != null) { + this.person.hasMotherStatus = true; if (profileObj.DataStatus.Mother == BioCheckPerson.CONF_WITH_DNA_STATUS) { this.person.motherDnaConfirmed = true; } @@ -313,7 +329,48 @@ export class BioCheckPerson { hasLocation() { return this.person.hasLocation; } - + /** + * Does profile have birth location + * @returns {Boolean} true if birth location + */ + hasBirthLocation() { + return this.person.hasBirthLocation; + } + /** + * Does profile have death location + * @returns {Boolean} true if death location + */ + hasDeathLocation() { + return this.person.hasDeathLocation; + } + /** + * Does profile have father + * @returns {Boolean} true if profile has father + */ + hasFather() { + return this.person.hasFather; + } + /** + * Does profile have mother + * @returns {Boolean} true if profile has mother + */ + hasMother() { + return this.person.hasMother; + } + /** + * Does profile have father status + * @returns {Boolean} true if profile has father status + */ + hasFatherStatus() { + return this.person.hasFatherStatus; + } + /** + * Does profile have mother status + * @returns {Boolean} true if profile has mother status + */ + hasMotherStatus() { + return this.person.hasMotherStatus; + } /** * Get the privacy * @returns {Number} numeric privacy level @@ -416,11 +473,24 @@ export class BioCheckPerson { if ((bLoc != null && bLoc.value.length > 0) || (dLoc != null && dLoc.value.length > 0)) { this.person.hasLocation = true; } + if (bLoc != null && bLoc.value.length > 0) { + this.person.hasBirthLocation = true; + } + if (dLoc != null && dLoc.value.length > 0) { + this.person.hasDeathLocation = true; + } + + // Determine if has father and has mother + let fatherEl = document.querySelector('#Father .tree--person'); + this.person.hasFather = !!(fatherEl && fatherEl.querySelector('a[href^="/wiki/"]')); + let motherEl = document.querySelector('#Mother .tree--person'); + this.person.hasMother = !!(motherEl && motherEl.querySelector('a[href^="/wiki/"]')); // get DNA confirmation status let val = document.getElementsByName("mStatus_Father"); for (let radio of val) { if (radio.checked) { + this.person.hasFatherStatus = true; if (radio.value == BioCheckPerson.CONF_WITH_DNA_STATUS) { this.person.fatherDnaConfirmed = true; } @@ -429,6 +499,7 @@ export class BioCheckPerson { val = document.getElementsByName("mStatus_Mother"); for (let radio of val) { if (radio.checked) { + this.person.hasMotherStatus = true; if (radio.value == BioCheckPerson.CONF_WITH_DNA_STATUS) { this.person.motherDnaConfirmed = true; } @@ -599,6 +670,18 @@ export class BioCheckPerson { isTooOldToRemember() { return this.#tooOldToRemember; } + /** + * Does the profile have a birth date + */ + hasBirthDate() { + return this.#hasBirthDate; + } + /** + * Does the profile have a death date + */ + hasDeathDate() { + return this.#hasDeathDate; + } /** * Does the profile lack dates * @returns {Boolean} true if profile has neither birth nor death date diff --git a/src/features/bioCheck/Biography.js b/src/features/bioCheck/Biography.js index ab9922a9..c2a4339a 100644 --- a/src/features/bioCheck/Biography.js +++ b/src/features/bioCheck/Biography.js @@ -78,6 +78,9 @@ export class Biography { #fatherDnaMarked = false; // is profile marked as father DNA confirmed? #motherDnaMarked = false; // is profile marked as mother DNA confirmed? + #bioScore = 0; // ranking based on bioCheck + #bioLineArray = []; // after == Biography == before Sources + // Hold results of parsing and validating a WikiTree biography #stats = { bioIsEmpty: false, @@ -85,9 +88,14 @@ export class Biography { bioIsMarkedUnsourced: false, bioIsUndated: false, totalBioLines: 0, + totalBioSectionLines: 0, // number non empty lines in biography + totalBioSectionChar: 0, // number characters in bio section inlineReferencesCount: 0, // number of possibleSourcesLineCount: 0, // number of lines that might contain sources bioHasProblems: false, + numberCategories: 0, + numberCategoriesNeeds: 0, + numberStickers: 0, }; #style = { bioHasNonCategoryTextBeforeBiographyHeading: false, @@ -196,6 +204,13 @@ export class Biography { this.#fatherDnaMarked = thePerson.person.fatherDnaConfirmed; this.#motherDnaMarked = thePerson.person.motherDnaConfirmed; + // update score from the person fields + this.#scorePerson(thePerson); + + if (inStr.length > 100000) { // 801 - Big Profile + this.#bioScore = this.#bioScore - 10; + } + this.#bioInputString = inStr; // Check for empty bio if (this.#bioInputString.length === 0) { @@ -266,6 +281,7 @@ export class Biography { // out of order if RNB, Project Box, Nav Box or Biography heading preceeds if (haveResearchNoteBox || haveNavBoxConfused || haveNavBoxSuccession || haveProjectBox || haveBiography || haveTextLine) { this.#style.bioCategoryNotAtStart = true; + this.#bioScore--; if (haveBiography) { this.#messages.styleMessages.push('Biography heading before ' + this.#bioLines[currentIndex]); } else { @@ -294,6 +310,10 @@ export class Biography { if (line.includes(Biography.#UNSOURCED)) { this.#stats.bioIsMarkedUnsourced = true; } + this.#stats.numberCategories++; + if (line.includes('needs')) { + this.#stats.numberCategoriesNeeds; + } // check for a location if profile has any if (thePerson.hasLocation()) { let str = line.replace('category:', ''); @@ -306,6 +326,7 @@ export class Biography { if (str.length <=0) { this.#style.bioHasStyleIssues = true; this.#messages.sectionMessages.push('Unsourced category does not have locations'); + this.#bioScore--; } } @@ -367,6 +388,7 @@ export class Biography { let msg = 'Navigation Box: ' + partialMixedCaseLine + ' is ' + stat + ' status'; this.#messages.styleMessages.push(msg); this.#style.bioHasStyleIssues = true; + this.#bioScore--; } else { if (partialLine.startsWith('easily confused')) { haveNavBoxConfused = true; @@ -389,6 +411,7 @@ export class Biography { } this.#messages.styleMessages.push(msg); this.#style.bioHasStyleIssues = true; + this.#bioScore--; } } if (partialLine.startsWith('succession')) { @@ -397,6 +420,7 @@ export class Biography { let msg = 'Navigation Box: ' + partialMixedCaseLine + ' should be before Biography heading'; this.#messages.styleMessages.push(msg); this.#style.bioHasStyleIssues = true; + this.#bioScore--; } } } @@ -417,6 +441,7 @@ export class Biography { } this.#messages.styleMessages.push(msg); this.#style.bioHasStyleIssues = true; + this.#bioScore--; } haveResearchNoteBox = true; this.#researchNoteBoxes.push(partialLine); @@ -426,6 +451,7 @@ export class Biography { let msg = 'Research Note Box: ' + partialMixedCaseLine + ' is ' + stat + ' status'; this.#messages.styleMessages.push(msg); this.#style.bioHasStyleIssues = true; + this.#bioScore--; } } else { if (this.#sourceRules.isProjectBox(partialLine)) { @@ -442,6 +468,7 @@ export class Biography { let msg = 'Project: ' + partialMixedCaseLine + ' should be before Biography heading'; this.#messages.styleMessages.push(msg); this.#style.bioHasStyleIssues = true; + this.#bioScore--; } } let stat = this.#sourceRules.getProjectBoxStatus(partialLine); @@ -449,19 +476,23 @@ export class Biography { let msg = 'Project Box: ' + partialMixedCaseLine + ' is ' + stat + ' status'; this.#messages.styleMessages.push(msg); this.#style.bioHasStyleIssues = true; + this.#bioScore--; } } else { if (this.#sourceRules.isSticker(partialLine)) { + this.#stats.numberStickers++; if (!haveBiography) { let msg = 'Sticker: ' + partialMixedCaseLine + ' should be after Biography heading'; this.#messages.styleMessages.push(msg); this.#style.bioHasStyleIssues = true; + this.#bioScore--; } let stat = this.#sourceRules.getStickerStatus(partialLine); if ((stat.length > 0) && (stat != 'approved')) { let msg = 'Sticker: ' + partialMixedCaseLine + ' is ' + stat + ' status'; this.#messages.styleMessages.push(msg); this.#style.bioHasStyleIssues = true; + this.#bioScore--; } } // end sticker } // end project box @@ -512,6 +543,7 @@ export class Biography { if (str.length <= 0) { this.#style.bioHasStyleIssues = true; this.#messages.styleMessages.push('Unsourced research note box does not have locations'); + this.#bioScore--; } } } @@ -520,11 +552,21 @@ export class Biography { let bioLineString = this.#getBioLineString(); this.#findRef(bioLineString); + // Get count before removing lines + let trimmedLines = this.#bioLines.filter(line => line && line.trim() !== ""); + this.#stats.totalBioLines = trimmedLines.length; + // Lose bio lines not considered to contain sources before testing sources this.#removeResearchNotes(); this.#removeAcknowledgements(); this.#removeAdvanceDirective(); + // Count the number of lines after biography heading + // Count the number of characters after biography heading + trimmedLines = this.#bioLineArray.filter(line => line && line.trim() !== ""); + this.#stats.totalBioSectionLines = trimmedLines.length; + // don't count the inline ref characters + this.#stats.totalBioSectionChar = trimmedLines.join().length - this.#refStringList.join().length; return; } @@ -579,24 +621,125 @@ export class Biography { this.#stats.bioIsUndated || this.#stats.bioIsMarkedUnsourced || this.#style.bioHasStyleIssues; + this.#score(); return isValid; } /** - * Validate using just a string of sources. This is typically - * used when adding a new person in basic mode. - * @param {String} sourcesStr string containing sources - * @param thePerson {BioCheckPerson} person to check - * @returns {Boolean} true if sources found. - */ - validateSourcesStr(sourcesStr, thePerson) { - // build bioLines from the input sources string then validate - this.#getLines(sourcesStr); - this.#isPre1500 = thePerson.isPre1500(); - this.#isPre1700 = thePerson.isPre1700(); - this.#tooOldToRemember = thePerson.isTooOldToRemember(); - this.#fatherDnaMarked = thePerson.person.fatherDnaConfirmed; - this.#motherDnaMarked = thePerson.person.motherDnaConfirmed; + * Get score value for the profile. This number may be less than 0. + * @returns score value for the profile + */ + getScore() { + return this.#bioScore; + } + /** + * Get Biography Section lines + * including each line after the == Biography == heading up through + * the start of the next heading or end of the biography. + * The \n character terminates a line, which are returned in an array. + * Blank lines are included. + * @returns {Array} of bio string lines + */ + getBioSectionLines() { + return this.#bioLineArray; // after == Biography == before Sources + } + + /* + * The following describes how Bio Check calculates a profile score + * + * Subtract points: + * -10 if a profile has no dates + * -10 if a profile is marked unsourced + * -10 if a profile is empty + * -10 if no valid sources found + * -10 if biography is over 100,000 characters + * -10 if there is no Biography section + * -4 if biography Section is empty + * -1 if biography Section is < 60 characters + * -1 for each invalid souce + * -1 for each source that is a span target + * -1 for each style issue + * -1 for number stickers beyond 5 + * -1 for each Needs_ category + * Add points + * +1 for each sticker to a max of 3 + * +1 if 4 stickers + * +1 for number categories, max of 4 + * +2 for each valid source inside a + * +1 for each source following Sources heading + * +1 for each 100 non empty characters in Biography section + */ + /* + * Score the profile based on results from parse and validate + * First parse, then validate, then score + * things that are counts are scored as parsed + * @returns a score value + */ + #score() { + + // Scoring number of non empty lines after Biography heading, before + // next heading or end of biography + if (this.#stats.totalBioSectionChar == 0) { + this.#bioScore = this.#bioScore - 4; + } else { + if (this.#stats.totalBioSectionChar < 60) { + this.#bioScore--; + } + let lineScore = this.#stats.totalBioSectionChar / 1000; + lineScore = Math.trunc(lineScore); + this.#bioScore = this.#bioScore + lineScore; + } + + if (this.#stats.bioIsMarkedUnsourced) { // can find in > 1 place only count once + this.#bioScore = this.#bioScore - 10; + } + if (this.#stats.bioIsEmpty) { + this.#bioScore = this.#bioScore - 10; + } + if (this.#style.misplacedLineCount > 0) { + this.#bioScore--; + } + this.#bioScore = this.#bioScore - this.#wrongLevelHeadings.length; // should be 3 not 2 + this.#bioScore = this.#bioScore - this.#refNamesMultiple.size; + this.#bioScore = this.#bioScore - this.#invalidSpanTargetList.length; + this.#bioScore = this.#bioScore - this.#missingRnb.length; + this.#bioScore = this.#bioScore - this.#sources.invalidDnaSourceList.length; + this.#bioScore = this.#bioScore + Math.min(this.#stats.numberCategories, 4); + this.#bioScore = this.#bioScore - this.#stats.numberCategoriesNeeds; + let stickerScore = 0; + if (this.#stats.numberStickers > 5) { + stickerScore = 5 - this.#stats.numberStickers; + } else { + if (this.#stats.numberStickers > 3) { + stickerScore = 3 - this.#stats.numberStickers; + } else { + stickerScore = this.#stats.numberStickers; + } + } + this.#bioScore = this.#bioScore + stickerScore; + // Scoring that depend on source validation + if (this.#sources.validSource.length == 0) { + this.#bioScore = this.#bioScore - 10; + } + this.#bioScore = this.#bioScore - this.#sources.invalidSource.length; + } + +/** + * Validate using just a string of sources. This is typically + * used when adding a new person in basic mode. + * @param {String} sourcesStr string containing sources + * @param thePerson {BioCheckPerson} person to check + * @returns {Boolean} true if sources found. + */ +validateSourcesStr(sourcesStr, thePerson) { + // build bioLines from the input sources string then validate + this.#getLines(sourcesStr); + this.#isPre1500 = thePerson.isPre1500(); + this.#isPre1700 = thePerson.isPre1700(); + this.#tooOldToRemember = thePerson.isTooOldToRemember(); + this.#fatherDnaMarked = thePerson.person.fatherDnaConfirmed; + this.#motherDnaMarked = thePerson.person.motherDnaConfirmed; + this.#scorePerson(thePerson); let isValid = this.#validateReferenceStrings(false); if (isValid) { this.#sources.sourcesFound = true; @@ -954,6 +1097,7 @@ export class Biography { } } else { this.#style.hasEndlessComment = true; + this.#bioScore--; pos = inStr.length + 1; // its an endless comment, just bail } } @@ -975,6 +1119,7 @@ export class Biography { ((str.indexOf(Biography.#END_BRACKET) < 0) || (str.indexOf(Biography.#END_BRACKET) < startPos))) { this.#style.bioHasBrWithoutEnd = true; + this.#bioScore--; } return outStr; } @@ -1081,6 +1226,7 @@ export class Biography { this.#biographyIndex = currentIndex; if (this.#advanceDirectiveIndex > 0) { this.#style.bioHasSectionAfterAdvanceDirective = true; + this.#bioScore--; } } else { if (this.#researchNotesIndex > 0) { @@ -1095,9 +1241,11 @@ export class Biography { if (this.#advanceDirectiveIndex > 0) { this.#style.bioHasSectionAfterAdvanceDirective = true; this.#advanceDirectiveEndIndex = currentIndex -1; + this.#bioScore--; } if (headingLevel > 2) { this.#style.sourcesHeadingHasExtraEqual = true; + this.#bioScore--; } if (this.#sourcesIndex < 0) { this.#sourcesIndex = currentIndex; @@ -1116,9 +1264,11 @@ export class Biography { } if (headingLevel > 2) { this.#style.acknowledgementsHeadingHasExtraEqual = true; + this.#bioScore--; } if (this.#sourcesIndex < 0) { this.#style.bioHasAcknowledgementsBeforeSources = true; + this.#bioScore--; } this.#acknowledgementsIndex = currentIndex; if (this.#researchNotesIndex > 0 && this.#researchNotesEndIndex < 0) { @@ -1129,6 +1279,7 @@ export class Biography { this.#advanceDirectiveIndex = currentIndex; if (headingLevel > 2) { this.#style.advanceDirectiveHeadingHasExtraEqual = true; + this.#bioScore--; } } else { if (headingLevel === 2) { @@ -1173,6 +1324,7 @@ export class Biography { endIndex = this.#acknowledgementsIndex; } startIndex++; + this.#bioLineArray = this.#bioLines.slice(startIndex, endIndex); // metrics if (this.#biographyIndex === endIndex) { this.#style.bioHeadingWithNoLinesFollowing = true; } else { @@ -1238,6 +1390,7 @@ export class Biography { if (!isJustRefName) { if (citeEnd < 0) { this.#style.hasRefWithoutEnd = true; + this.#bioScore--; } else { let line = refArray[i].substring(citeStart, citeEnd); this.#refStringList.push(line); @@ -1266,6 +1419,7 @@ export class Biography { if (nameEnd < 0) { // malformed ref this.#style.hasRefWithoutEnd = true; + this.#bioScore--; } } if (nameEnd > nameStart) { @@ -1316,6 +1470,7 @@ export class Biography { if (this.#biographyIndex < 0) { this.#style.bioHasStyleIssues = true; this.#style.bioIsMissingBiographyHeading = true; + this.#bioScore = this.#bioScore - 10; this.#messages.sectionMessages.push('Missing Biography heading'); } else { if (this.#unexpectedLines.length > 0) { @@ -1335,14 +1490,17 @@ export class Biography { if (this.#sourcesIndex < 0) { this.#style.bioHasStyleIssues = true; this.#style.bioIsMissingSourcesHeading = true; + this.#bioScore--; this.#messages.sectionMessages.push('Missing Sources heading'); } if (this.#referencesIndex < 0) { this.#style.bioHasStyleIssues = true; this.#style.bioIsMissingReferencesTag = true; + this.#bioScore--; this.#messages.sectionMessages.push('Missing tag'); } if (this.#style.bioHasMultipleReferencesTags) { + this.#bioScore--; this.#style.bioHasStyleIssues = true; this.#messages.sectionMessages.push('Multiple tag'); } @@ -1384,6 +1542,7 @@ export class Biography { if (!this.#refNamesDefined.has(refName)) { this.#style.bioHasStyleIssues = true; this.#messages.sectionMessages.push('Inline ' + refName + ' has no citation'); + this.#bioScore--; } } @@ -1434,6 +1593,7 @@ export class Biography { this.#messages.sectionMessages.push('Advance Directive is not at end of profile'); } if (this.#style.advanceDirectiveOnNonMemberProfile) { + this.#bioScore--; this.#style.bioHasStyleIssues = true; this.#messages.styleMessages.push('Advance Directive on a non member profile'); } @@ -1447,6 +1607,7 @@ export class Biography { if (this.#style.bioMightHaveEmail) { this.#style.bioHasStyleIssues = true; this.#messages.styleMessages.push('Biography may contain email address'); + this.#bioScore--; } // Report DNA confirmation results. @@ -1466,18 +1627,22 @@ export class Biography { if (this.#fatherDnaMarked && !this.#style.bioHasPaternalDnaConf) { this.#style.bioHasStyleIssues = true; this.#messages.styleMessages.push("Missing father's DNA confirmation source"); + this.#bioScore--; } if (this.#motherDnaMarked && !this.#style.bioHasMaternalDnaConf) { this.#style.bioHasStyleIssues = true; this.#messages.styleMessages.push("Missing mother's DNA confirmation source"); + this.#bioScore--; } if (!this.#fatherDnaMarked && this.#style.bioHasPaternalDnaConf) { this.#style.bioHasStyleIssues = true; this.#messages.styleMessages.push("Father not marked as Confirmed with DNA"); + this.#bioScore--; } if (!this.#motherDnaMarked && this.#style.bioHasMaternalDnaConf) { this.#style.bioHasStyleIssues = true; this.#messages.styleMessages.push("Mother not marked as Confirmed with DNA"); + this.#bioScore--; } } @@ -1493,6 +1658,7 @@ export class Biography { if (isBioHeading) { if (this.#bioHeadingsFound.includes(line)) { this.#style.bioHasMultipleBioHeadings = true; + this.#bioScore--; } else { this.#bioHeadingsFound.push(line); } @@ -1512,6 +1678,7 @@ export class Biography { if (isSourcesHeading) { if (this.#sourcesHeadingsFound.includes(line)) { this.#style.bioHasMultipleSourceHeadings = true; + this.#bioScore--; } else { this.#sourcesHeadingsFound.push(line); } @@ -1663,12 +1830,14 @@ export class Biography { this.#messages.styleMessages.push('Horizontal rule before Biography'); this.#style.bioHasStyleIssues = true; this.#headingBeforeBiography = true; + this.#bioScore--; } else { if (line.startsWith(Biography.#HEADING_START)) { if (!this.#headingBeforeBiography) { this.#style.bioHasStyleIssues = true; this.#headingBeforeBiography = true; this.#messages.styleMessages.push('Heading or subheading before Biography'); + this.#bioScore--; } } else { if ((line.startsWith('[[')) && (line.endsWith(']]'))) { @@ -1698,6 +1867,7 @@ export class Biography { } this.#messages.styleMessages.push(msg); this.#style.bioHasStyleIssues = true; + this.#bioScore--; } } return; @@ -1731,6 +1901,7 @@ export class Biography { let msg = templateName + ' template has duplicate parameter ' + paramName; this.#messages.styleMessages.push(msg); this.#style.bioHasStyleIssues = true; + this.#bioScore--; } else { paramNameSet.add(paramNameLower); } @@ -1933,6 +2104,7 @@ export class Biography { } } else { if (this.#isValidSource(line)) { + this.#bioScore = this.#bioScore + 2; if (!isValid) { // first one found? isValid = true; @@ -2021,6 +2193,7 @@ export class Biography { // Unless all the ref are between Sources and references if (line.indexOf("= 0 && index > this.#referencesIndex) { this.#style.bioHasRefAfterReferences = true; + this.#bioScore--; } // Only count misplaced line if there is a references tag if (index < this.#referencesIndex && this.#referencesIndex > 0) { @@ -2029,6 +2202,7 @@ export class Biography { let spanTargetStartPos = mixedCaseLine.indexOf(Biography.#SPAN_TARGET_START); if (spanTargetStartPos < 0) { if (this.#isValidSource(mixedCaseLine)) { + this.#bioScore++; if (!isValid) { isValid = true; // first one found } @@ -2072,6 +2246,7 @@ export class Biography { pos = pos + Biography.#SPAN_TARGET_END.length; } else { this.#style.bioHasSpanWithoutEndingSpan = true; + this.#bioScore--; pos = mixedCaseLine.length; } if (pos < mixedCaseLine.length) { @@ -2430,10 +2605,8 @@ export class Biography { this.#style.bioHasMaternalDnaConf = true; } } else { -//console.log('incomplete confirmation ' + line); -//console.log('detectedType ' + detectedType); -//console.log(this.#dnaReason); this.#style.bioHasIncompleteDNAconfirmation = true; + this.#bioScore--; } // Save for reporting let dnaSource = { @@ -2877,4 +3050,43 @@ export class Biography { } return isJustCombined; } + + + /* ********************************************************************* + * ******************* PRIVATE METHODS ********************************* + * ******************* used for Scoring ******************************** + * ********************************************************************* + */ + /* + * Set profile score items from person + * @param thePerson {BioCheckPerson} person + */ + #scorePerson(thePerson) { + /* + this.#scoreBoolean(thePerson.hasBirthDate()); + this.#scoreBoolean(thePerson.hasDeathDate()); + this.#scoreBoolean(thePerson.hasBirthLocation()); + this.#scoreBoolean(thePerson.hasDeathLocation()); + this.#scoreBoolean(thePerson.hasFather()); + this.#scoreBoolean(thePerson.hasMother()); + this.#scoreBoolean(thePerson.hasFatherStatus()); + this.#scoreBoolean(thePerson.hasMotherStatus()); + */ + if (thePerson.isUndated()) { + this.#bioScore = this.#bioScore - 10; + } + } + + /* + * Score boolean + * @param {Boolean} does wanted item exist + * if true add 1 else false missing subtract 1 + */ + #scoreBoolean(hasItem) { + if (hasItem) { + this.#bioScore++; + } else { + this.#bioScore--; + } + } } diff --git a/src/features/bioCheck/README.md b/src/features/bioCheck/README.md index 5dc7ac0f..70a5cf40 100644 --- a/src/features/bioCheck/README.md +++ b/src/features/bioCheck/README.md @@ -35,6 +35,9 @@ import { Biography } from "./Biography.js"; // optionally determine if biography has any problems let hasProblems = biography.hasProblems(); + + // optionally get the bioScore + let bioScore = biography.getScore(); } @@ -141,6 +144,42 @@ Does profile have either birth or death location Returns **[Boolean][7]** true if either location present +### hasBirthLocation + +Does profile have birth location + +Returns **[Boolean][7]** true if birth location + +### hasDeathLocation + +Does profile have death location + +Returns **[Boolean][7]** true if death location + +### hasFather + +Does profile have father + +Returns **[Boolean][7]** true if profile has father + +### hasMother + +Does profile have mother + +Returns **[Boolean][7]** true if profile has mother + +### hasFatherStatus + +Does profile have father status + +Returns **[Boolean][7]** true if profile has father status + +### hasMotherStatus + +Does profile have mother status + +Returns **[Boolean][7]** true if profile has mother status + ### getPrivacy Get the privacy @@ -190,6 +229,14 @@ Is the person born > 150 years ago or died > 100 years ago Returns **[Boolean][7]** true if born > 150 years ago or died > 100 years ago +### hasBirthDate + +Does the profile have a birth date + +### hasDeathDate + +Does the profile have a death date + ### isUndated Does the profile lack dates @@ -292,6 +339,22 @@ Returns **[Boolean][7]** true if profile looks good, else false. Returns false a profile that appears unsourced (is ?), a profile with an empty bio, a profile with no dates, or a profile that has an Unsourced Research Notes Box or is in an Unsourced category. +### getScore + +Get score value for the profile. This number may be less than 0. + +Returns **any** score value for the profile + +### getBioSectionLines + +Get Biography Section lines +including each line after the == Biography == heading up through +the start of the next heading or end of the biography. +The \n character terminates a line, which are returned in an array. +Blank lines are included. + +Returns **[Array][11]** of bio string lines + ### validateSourcesStr Validate using just a string of sources. This is typically @@ -705,7 +768,7 @@ Returns **[Boolean][7]** true if sticker else false ### isRecommendedTag -Determine if a line starts with an HTML tag +Determine if a line contains with an HTML tag that is recommended for use on WikiTree. Typically used for a line that starts with < diff --git a/src/features/bioCheck/bioCheck.js b/src/features/bioCheck/bioCheck.js index 2de8f946..bbf21061 100644 --- a/src/features/bioCheck/bioCheck.js +++ b/src/features/bioCheck/bioCheck.js @@ -163,6 +163,12 @@ function buildReportLines(container, bioStatus, biography, isPre1700) { bioResultItem.appendChild(document.createTextNode(msg)); container.appendChild(bioResultItem); + bioResultItem = document.createElement("li"); + msg = "Bio Score: " + biography.getScore(); + bioResultItem.appendChild(document.createTextNode(msg)); + container.appendChild(bioResultItem); + + let numBadSources = biography.getInvalidSources().length; if (biography.getInvalidSources().length > 0) { let bioResultItem = document.createElement("li"); From 47f2f95f2d5077b7024759f2b0ee16da5afc01b9 Mon Sep 17 00:00:00 2001 From: ke4tch Date: Wed, 25 Mar 2026 11:40:27 -0400 Subject: [PATCH 3/7] Handle biological parents; improve DNA confirmation checking; improve source checking --- src/features/bioCheck/BioCheckPerson.js | 88 ++++++++----------------- src/features/bioCheck/Biography.js | 39 +++++------ src/features/bioCheck/SourceRules.js | 7 +- 3 files changed, 52 insertions(+), 82 deletions(-) diff --git a/src/features/bioCheck/BioCheckPerson.js b/src/features/bioCheck/BioCheckPerson.js index 6bec3068..8bfc0158 100644 --- a/src/features/bioCheck/BioCheckPerson.js +++ b/src/features/bioCheck/BioCheckPerson.js @@ -1,7 +1,7 @@ /* The MIT License (MIT) -Copyright (c) 2025 Kathryn J Knight +Copyright (c) 2026 Kathryn J Knight Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in @@ -40,7 +40,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * Id,Name,IsLiving,Privacy,Manager,IsMember, * BirthDate,DeathDate,BirthDateDecade,DeathDateDecade, * BirthLocation,DeathLocation, - * FirstName,RealName,LastNameCurrent,LastNameAtBirth,Mother,Father,DataStatus,Bio + * FirstName,RealName,LastNameCurrent,LastNameAtBirth,DataStatus,Bio */ export class BioCheckPerson { #birthDate = null; // as a Date object @@ -71,10 +71,6 @@ export class BioCheckPerson { hasBirthLocation: false, hasDeathLocation: false, uncheckedDueToPrivacy: false, - hasFather: false, - hasMother: false, - hasFatherStatus: false, - hasMotherStatus: false, uncheckedDueToDate: false, fatherDnaConfirmed: false, motherDnaConfirmed: false, @@ -175,25 +171,15 @@ export class BioCheckPerson { this.person.hasLocation = true; this.person.hasDeathLocation = true; } - if (profileObj.Father != null) { - this.person.hasFather = true; + + // To check for DNA status use Bio parent if not null else default to just parent + if ((profileObj.DataStatus.BioFather == BioCheckPerson.CONF_WITH_DNA_STATUS) || + (profileObj.DataStatus.Father == BioCheckPerson.CONF_WITH_DNA_STATUS)) { + this.person.fatherDnaConfirmed = true; } - if (profileObj.Mother != null) { - this.person.hasMother = true; - } - if (profileObj.DataStatus != null) { - if (profileObj.DataStatus.Father != null) { - this.person.hasFatherStatus = true; - if (profileObj.DataStatus.Father == BioCheckPerson.CONF_WITH_DNA_STATUS) { - this.person.fatherDnaConfirmed = true; - } - } - if (profileObj.DataStatus.Mother != null) { - this.person.hasMotherStatus = true; - if (profileObj.DataStatus.Mother == BioCheckPerson.CONF_WITH_DNA_STATUS) { - this.person.motherDnaConfirmed = true; - } - } + if ((profileObj.DataStatus.BioMother == BioCheckPerson.CONF_WITH_DNA_STATUS) || + (profileObj.DataStatus.Mother == BioCheckPerson.CONF_WITH_DNA_STATUS)) { + this.person.motherDnaConfirmed = true; } // can use if logged in user is the same as Manager if (this.person.privacyLevel < BioCheckPerson.MIN_PRIVACY) { @@ -343,34 +329,6 @@ export class BioCheckPerson { hasDeathLocation() { return this.person.hasDeathLocation; } - /** - * Does profile have father - * @returns {Boolean} true if profile has father - */ - hasFather() { - return this.person.hasFather; - } - /** - * Does profile have mother - * @returns {Boolean} true if profile has mother - */ - hasMother() { - return this.person.hasMother; - } - /** - * Does profile have father status - * @returns {Boolean} true if profile has father status - */ - hasFatherStatus() { - return this.person.hasFatherStatus; - } - /** - * Does profile have mother status - * @returns {Boolean} true if profile has mother status - */ - hasMotherStatus() { - return this.person.hasMotherStatus; - } /** * Get the privacy * @returns {Number} numeric privacy level @@ -480,17 +438,27 @@ export class BioCheckPerson { this.person.hasDeathLocation = true; } - // Determine if has father and has mother - let fatherEl = document.querySelector('#Father .tree--person'); - this.person.hasFather = !!(fatherEl && fatherEl.querySelector('a[href^="/wiki/"]')); - let motherEl = document.querySelector('#Mother .tree--person'); - this.person.hasMother = !!(motherEl && motherEl.querySelector('a[href^="/wiki/"]')); - // get DNA confirmation status - let val = document.getElementsByName("mStatus_Father"); + let val = document.getElementsByName("mStatus_BioFather"); + for (let radio of val) { + if (radio.checked) { + if (radio.value == BioCheckPerson.CONF_WITH_DNA_STATUS) { + this.person.fatherDnaConfirmed = true; + } + } + } + val = document.getElementsByName("mStatus_BioMother"); + for (let radio of val) { + if (radio.checked) { + this.person.hasMotherStatus = true; + if (radio.value == BioCheckPerson.CONF_WITH_DNA_STATUS) { + this.person.motherDnaConfirmed = true; + } + } + } + val = document.getElementsByName("mStatus_Father"); for (let radio of val) { if (radio.checked) { - this.person.hasFatherStatus = true; if (radio.value == BioCheckPerson.CONF_WITH_DNA_STATUS) { this.person.fatherDnaConfirmed = true; } diff --git a/src/features/bioCheck/Biography.js b/src/features/bioCheck/Biography.js index c2a4339a..a5227b6c 100644 --- a/src/features/bioCheck/Biography.js +++ b/src/features/bioCheck/Biography.js @@ -5,7 +5,7 @@ Created By: Kay Knight (Sands-1865) /* The MIT License (MIT) -Copyright (c) 2025 Kathryn J Knight +Copyright (c) 2026 Kathryn J Knight Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in @@ -710,10 +710,13 @@ export class Biography { if (this.#stats.numberStickers > 5) { stickerScore = 5 - this.#stats.numberStickers; } else { - if (this.#stats.numberStickers > 3) { - stickerScore = 3 - this.#stats.numberStickers; + stickerScore = this.#stats.numberStickers; + if (this.#stats.numberStickers == 4) { + stickerScore = 1; } else { - stickerScore = this.#stats.numberStickers; + if (this.#stats.numberStickers == 5) { + stickerScore = 0; + } } } this.#bioScore = this.#bioScore + stickerScore; @@ -2560,7 +2563,7 @@ validateSourcesStr(sourcesStr, thePerson) { line = line.replace(/ancestry\s(dna)?/, ""); let mrcaStartIndex = this.#getMrcaStartIndex(line); - let wikiTreeIdCount = this.#lineWikiTreeIdCount(line, mrcaStartIndex); // guess how many WikiTree IDs are found + let wikiTreeIdCount = this.#lineWikiTreeIdCount(line, mrcaStartIndex); if (wikiTreeIdCount > 0) { let cM = this.#getCm(line); if (this.#isTriangulation(line)) { @@ -2782,6 +2785,7 @@ validateSourcesStr(sourcesStr, thePerson) { line = line.replaceAll('grandfather', ''); return ((line.includes('paternal') || line.includes('father') || line.includes('paternity') || line.includes('parental')) || + line.includes('patrilineal') || line.includes(' parents confirm') && (line.includes('relation') || line.includes('descent') || line.includes('line'))); @@ -2793,6 +2797,7 @@ validateSourcesStr(sourcesStr, thePerson) { line = line.replaceAll('grandmother', ''); return ((line.includes('maternal') || line.includes('mother') || line.includes('maternity') || line.includes('parental')) || + line.includes('matrilineal') || line.includes(' parents confirm') && (line.includes('relation') || line.includes('descent') || line.includes('line'))); @@ -2818,6 +2823,8 @@ validateSourcesStr(sourcesStr, thePerson) { /* * Does line appear to have a WikiTree-ID + * Handle the case where the LNAB itself has the - character + * You need the count to check for autosomal without MRCA */ #lineWikiTreeIdCount(line, mrcaStartIndex) { line = line.replace('y-chromosome', 'y chromosome'); @@ -2833,11 +2840,11 @@ validateSourcesStr(sourcesStr, thePerson) { let bigParts = line.split(/[,;|\[\]]/); for (let i = 0; i < bigParts.length; i++) { if (bigParts[i].includes('-')) { - let littleParts = bigParts[i].split('-'); - if (littleParts.length === 2) { - if (this.#looksLikeWikiTreeId(littleParts[0].trim(), littleParts[1].trim())) { - idCount++; - } + let dashAt = bigParts[i].lastIndexOf('-'); + let namePart = bigParts[i].substring(0, dashAt); + let numberPart = bigParts[i].substring(dashAt); + if (this.#looksLikeWikiTreeId(namePart.trim(), numberPart.trim())) { + idCount++; } } } @@ -2852,7 +2859,7 @@ validateSourcesStr(sourcesStr, thePerson) { #looksLikeWikiTreeId(namePart, numberPart) { namePart = namePart.replace('https://www.wikitree.com/wiki/', ''); - let isAlpha = /^[a-zA-Z\u00C0-\u017F_]+$/.test(namePart); + let isAlpha = /^[a-zA-Z\u00C0-\u017F_\-]+$/.test(namePart); let numbers = numberPart.split(' ', 1); let isNumeric = /\d/.test(numbers[0]); @@ -3062,16 +3069,6 @@ validateSourcesStr(sourcesStr, thePerson) { * @param thePerson {BioCheckPerson} person */ #scorePerson(thePerson) { - /* - this.#scoreBoolean(thePerson.hasBirthDate()); - this.#scoreBoolean(thePerson.hasDeathDate()); - this.#scoreBoolean(thePerson.hasBirthLocation()); - this.#scoreBoolean(thePerson.hasDeathLocation()); - this.#scoreBoolean(thePerson.hasFather()); - this.#scoreBoolean(thePerson.hasMother()); - this.#scoreBoolean(thePerson.hasFatherStatus()); - this.#scoreBoolean(thePerson.hasMotherStatus()); - */ if (thePerson.isUndated()) { this.#bioScore = this.#bioScore - 10; } diff --git a/src/features/bioCheck/SourceRules.js b/src/features/bioCheck/SourceRules.js index 3a6715de..d716858f 100644 --- a/src/features/bioCheck/SourceRules.js +++ b/src/features/bioCheck/SourceRules.js @@ -1,7 +1,7 @@ /* The MIT License (MIT) -Copyright (c) 2025 Kathryn J Knight +Copyright (c) 2026 Kathryn J Knight Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in @@ -309,6 +309,7 @@ class SourceRules { "family tree files", "family search lds", "fellow researcher", + "legacy nfs source", "my family records", "on going research", "scotland's people", @@ -441,6 +442,7 @@ class SourceRules { "u.s., new england marriages prior to 1700", "geneanet community trees index on ancestry", "marriage records and ancestry.com research", + "england select deaths and burials 1538-1991", "north carolina, marriage records, 1741-2011", "illinois statewide marriage index, 1763–1900", "maternal relationship confirmed by dna match", @@ -456,6 +458,7 @@ class SourceRules { "from behind the ancestry.com subscription wall:", "personal knowledge, newspaper and bible records", "us and international marriage records 1560-1900", + "england select births and christenings 1538-1975", "massachusetts, town and vital records, 1620-1988", "paternal & maternal relationship is confirmed by:", "replace this citation if there is another source", @@ -472,6 +475,7 @@ class SourceRules { "us federal census mortality schedules index 1850-1880", "u.s., world war i draft registration cards, 1917-1918", "u.s., civil war draft registrations records, 1863-1865", + "england & wales civil registration death index 1837-1915", "u.s., new england marriages prior to 1700 (ancestry.com)", "maternal relationship is confirmed by dna as described above", "paternal relationship is confirmed by dna as described above", @@ -844,6 +848,7 @@ class SourceRules { "family member", "family papers", "family search", + "kmm genealogy", "needs sources", ":source list:", "source needed", From 291b66e9a4727477f3df741e7df12474a0d9d34b Mon Sep 17 00:00:00 2001 From: ke4tch Date: Sat, 25 Apr 2026 15:08:37 -0400 Subject: [PATCH 4/7] Check Notability template position; Number of stickers --- src/features/bioCheck/BioCheckPerson.js | 19 ++++--- src/features/bioCheck/Biography.js | 76 ++++++++++++++++++++----- src/features/bioCheck/SourceRules.js | 32 +++++++++++ 3 files changed, 106 insertions(+), 21 deletions(-) diff --git a/src/features/bioCheck/BioCheckPerson.js b/src/features/bioCheck/BioCheckPerson.js index 8bfc0158..f4890f29 100644 --- a/src/features/bioCheck/BioCheckPerson.js +++ b/src/features/bioCheck/BioCheckPerson.js @@ -172,14 +172,17 @@ export class BioCheckPerson { this.person.hasDeathLocation = true; } - // To check for DNA status use Bio parent if not null else default to just parent - if ((profileObj.DataStatus.BioFather == BioCheckPerson.CONF_WITH_DNA_STATUS) || - (profileObj.DataStatus.Father == BioCheckPerson.CONF_WITH_DNA_STATUS)) { - this.person.fatherDnaConfirmed = true; - } - if ((profileObj.DataStatus.BioMother == BioCheckPerson.CONF_WITH_DNA_STATUS) || - (profileObj.DataStatus.Mother == BioCheckPerson.CONF_WITH_DNA_STATUS)) { - this.person.motherDnaConfirmed = true; + // note that DataStatus might not be returned, depending on privacy + if (profileObj.DataStatus != null) { + // To check for DNA status use Bio parent if not null else default to just parent + if ((profileObj.DataStatus.BioFather == BioCheckPerson.CONF_WITH_DNA_STATUS) || + (profileObj.DataStatus.Father == BioCheckPerson.CONF_WITH_DNA_STATUS)) { + this.person.fatherDnaConfirmed = true; + } + if ((profileObj.DataStatus.BioMother == BioCheckPerson.CONF_WITH_DNA_STATUS) || + (profileObj.DataStatus.Mother == BioCheckPerson.CONF_WITH_DNA_STATUS)) { + this.person.motherDnaConfirmed = true; + } } // can use if logged in user is the same as Manager if (this.person.privacyLevel < BioCheckPerson.MIN_PRIVACY) { diff --git a/src/features/bioCheck/Biography.js b/src/features/bioCheck/Biography.js index a5227b6c..8333bf25 100644 --- a/src/features/bioCheck/Biography.js +++ b/src/features/bioCheck/Biography.js @@ -106,6 +106,7 @@ export class Biography { bioHasMultipleBioHeadings: false, bioHasRefWithoutEnd: false, bioHasSpanWithoutEndingSpan: false, + bioHasTooManyStickers: false, bioIsMissingSourcesHeading: false, sourcesHeadingHasExtraEqual: false, bioHasMultipleSourceHeadings: false, @@ -237,7 +238,10 @@ export class Biography { let haveNavBoxSuccession = false; let haveProjectBox = false; let haveBiography = false; - let haveTextLine = false; + let haveTextLine = false; // before Biography heading + let haveBioText = false; // after Biography heading + let haveNotabilityTemplate = false; + let isBioHeadingLine = false; // build a vector of each line in the bio then iterate this.#getLines(this.#bioInputString); @@ -250,6 +254,7 @@ export class Biography { while (currentIndex < lineCount) { let mixedCaseLine = this.#bioLines[currentIndex].trim(); let line = this.#bioLines[currentIndex].toLowerCase().trim(); + isBioHeadingLine = false; let linesToSkip = 0; if (line.length > 0) { // something here? if (line.indexOf(Biography.#REFERENCES_TAG) >= 0) { @@ -259,6 +264,7 @@ export class Biography { this.#evaluateHeadingLine(line, currentIndex, this.#bioLines[currentIndex]); if (this.#biographyIndex >= 0) { haveBiography = true; + isBioHeadingLine = true; } } if (this.#checkForEmail(line)) { @@ -279,7 +285,8 @@ export class Biography { // Report category out of order with the last thing reported first so that // you only get one reported per category // out of order if RNB, Project Box, Nav Box or Biography heading preceeds - if (haveResearchNoteBox || haveNavBoxConfused || haveNavBoxSuccession || haveProjectBox || haveBiography || haveTextLine) { + if (haveResearchNoteBox || haveNavBoxConfused || haveNavBoxSuccession || haveProjectBox || haveBiography || + haveTextLine || haveNotabilityTemplate) { this.#style.bioCategoryNotAtStart = true; this.#bioScore--; if (haveBiography) { @@ -299,6 +306,10 @@ export class Biography { } else { if (haveNavBoxConfused) { this.#messages.styleMessages.push('Easily Confused Navigation Box before ' + this.#bioLines[currentIndex]); + } else { + if (haveNotabilityTemplate) { + this.#messages.styleMessages.push('Notability statement before ' + this.#bioLines[currentIndex]); + } } } } @@ -479,22 +490,46 @@ export class Biography { this.#bioScore--; } } else { - if (this.#sourceRules.isSticker(partialLine)) { - this.#stats.numberStickers++; + if (this.#sourceRules.isNotabilityTemplate(partialLine)) { + // Must be after Biography before any stickers if (!haveBiography) { - let msg = 'Sticker: ' + partialMixedCaseLine + ' should be after Biography heading'; - this.#messages.styleMessages.push(msg); - this.#style.bioHasStyleIssues = true; - this.#bioScore--; - } - let stat = this.#sourceRules.getStickerStatus(partialLine); - if ((stat.length > 0) && (stat != 'approved')) { - let msg = 'Sticker: ' + partialMixedCaseLine + ' is ' + stat + ' status'; + let msg = 'Notability statement should be after Biography heading'; this.#messages.styleMessages.push(msg); this.#style.bioHasStyleIssues = true; this.#bioScore--; + } else { + if (this.#stats.numberStickers > 0) { + let msg = 'Notability statement should be before any Sticker'; + this.#messages.styleMessages.push(msg); + this.#style.bioHasStyleIssues = true; + this.#bioScore--; + } else { + if (haveBioText) { + let msg = 'Notability statement should be after Biography heading and before any text'; + this.#messages.styleMessages.push(msg); + this.#style.bioHasStyleIssues = true; + this.#bioScore--; + } + } } - } // end sticker + } else { + if (this.#sourceRules.isSticker(partialLine)) { + this.#stats.numberStickers++; + if (!haveBiography) { + let msg = 'Sticker: ' + partialMixedCaseLine + ' should be after Biography heading'; + this.#messages.styleMessages.push(msg); + this.#style.bioHasStyleIssues = true; + this.#bioScore--; + } + let stat = this.#sourceRules.getStickerStatus(partialLine); + if ((stat.length > 0) && (stat != 'approved')) { + let msg = 'Sticker: ' + partialMixedCaseLine + ' is ' + stat + ' status'; + this.#messages.styleMessages.push(msg); + this.#style.bioHasStyleIssues = true; + this.#bioScore--; + } + } // end sticker + } } // end project box } // end research note box } // end nav box @@ -508,6 +543,10 @@ export class Biography { // test the line before the bio this.#checkLineBeforeBio(str); } + } else { + if (!isBioHeadingLine) { + haveBioText = true; + } } } } @@ -548,6 +587,12 @@ export class Biography { } } + // Check for too many stickers + if ((this.#stats.numberStickers > 5) && (!thePerson.isMember())) { + this.#style.bioHasTooManyStickers = true; + this.#style.bioHasStyleIssues = true; + } + // Get the string that might contain xxx pairs let bioLineString = this.#getBioLineString(); this.#findRef(bioLineString); @@ -1579,6 +1624,11 @@ validateSourcesStr(sourcesStr, thePerson) { this.#messages.styleMessages.push('Wrong level heading == ' + str + ' =='); } } + if (this.#style.bioHasTooManyStickers) { + this.#style.bioHasStyleIssues = true; + this.#bioScore--; + this.#messages.sectionMessages.push('Too many Stickers: ' + this.#stats.numberStickers); + } if (this.#style.acknowledgementsHeadingHasExtraEqual) { this.#style.bioHasStyleIssues = true; this.#messages.sectionMessages.push('Acknowledgements subsection instead of section'); diff --git a/src/features/bioCheck/SourceRules.js b/src/features/bioCheck/SourceRules.js index d716858f..a30c0620 100644 --- a/src/features/bioCheck/SourceRules.js +++ b/src/features/bioCheck/SourceRules.js @@ -170,6 +170,8 @@ class SourceRules { #projectBox = []; // loads from templates, each is name and status #sticker = []; + // loads from templates, each is name and status + #formattingTemplate = []; // recommended HTML tags #recommendedTagsStart = [ @@ -999,6 +1001,17 @@ class SourceRules { projectBox.status = templates[i].status.toLowerCase().trim(); this.#projectBox.push(projectBox); } + // TODO hack that Notability may be set to the wrong type + if ((templates[i].type.toLowerCase().trim() === 'formatting') || + (templates[i].type.toLowerCase().trim() === 'formattingtemplate')) { + let formattingTemplate = { + name: "", + status: "", + }; + formattingTemplate.name = templates[i].name.toLowerCase().trim(); + formattingTemplate.status = templates[i].status.toLowerCase().trim(); + this.#formattingTemplate.push(formattingTemplate); + } if (templates[i].type.toLowerCase().trim() === 'sticker') { let sticker = { name: "", @@ -1191,6 +1204,25 @@ class SourceRules { return isFound; } + /** + * Determine if line is a Notability template + * Do not want to look at all formatting templates, at present + * assumes the leading {{ removed and line is lower case + * @param {String} line to test + * @returns {Boolean} true if notability else false + */ + isNotabilityTemplate(line) { + let isFound = false; + if (line === 'notability') { + this.#formattingTemplate.find((element) => { + if (element.name === line) { + isFound = true; + } + }); + } + return isFound; + } + /** * Determine if a line is a valid Sticker * assumes the leading {{ removed and line is lower case From c350aa5de9a4471594d9d1a3b92a797309ef8fcc Mon Sep 17 00:00:00 2001 From: ke4tch Date: Sun, 26 Apr 2026 15:11:02 -0400 Subject: [PATCH 5/7] Report when profile has neither birth nor death locations; Update documentation --- src/features/bioCheck/BioCheckPerson.js | 207 +++++++++++++----------- src/features/bioCheck/Biography.js | 13 ++ src/features/bioCheck/README.md | 46 +++--- 3 files changed, 149 insertions(+), 117 deletions(-) diff --git a/src/features/bioCheck/BioCheckPerson.js b/src/features/bioCheck/BioCheckPerson.js index f4890f29..2aecca11 100644 --- a/src/features/bioCheck/BioCheckPerson.js +++ b/src/features/bioCheck/BioCheckPerson.js @@ -39,7 +39,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * Expects the profile to contain the following fields from the API: * Id,Name,IsLiving,Privacy,Manager,IsMember, * BirthDate,DeathDate,BirthDateDecade,DeathDateDecade, - * BirthLocation,DeathLocation, + * BirthLocation,DeathLocation,Managers, * FirstName,RealName,LastNameCurrent,LastNameAtBirth,DataStatus,Bio */ export class BioCheckPerson { @@ -74,6 +74,7 @@ export class BioCheckPerson { uncheckedDueToDate: false, fatherDnaConfirmed: false, motherDnaConfirmed: false, + managers: [], }; static TOO_OLD_TO_REMEMBER_DEATH = 100; @@ -136,110 +137,122 @@ export class BioCheckPerson { this.person.bio = ""; // Even if something returned, we can't process it without a Name if (profileObj.Name != null) { - this.person.wikiTreeId = profileObj.Name; - this.person.hasName = true; - if (profileObj.Manager != null) { - this.person.managerId = profileObj.Manager; - } - if (profileObj.Privacy != null) { - this.person.privacyLevel = profileObj.Privacy; - } - if (profileObj.IsMember != null) { - if (profileObj.IsMember === 1) { - this.person.isMember = true; - } - } - if (profileObj.FirstName != null) { - this.person.firstName = profileObj.FirstName; + if ((profileObj.Name.startsWith('WikiTree-')) || (profileObj.Name.startsWith('Example-'))) { + canUseThis = false; // do not count as ignored due to xxx } else { - if (profileObj.RealName != null) { - this.person.firstName = profileObj.RealName; + this.person.wikiTreeId = profileObj.Name; + this.person.hasName = true; + if (profileObj.Manager != null) { + this.person.managerId = profileObj.Manager; } - } - if (profileObj.LastNameCurrent != null) { - this.person.lastName = profileObj.LastNameCurrent; - } else { - if (profileObj.LastNameAtBirth != null) { - this.person.lastName = profileObj.LastNameAtBirth; + if (profileObj.Privacy != null) { + this.person.privacyLevel = profileObj.Privacy; } - } - if (profileObj.BirthLocation != null && profileObj.BirthLocation.length > 0) { - this.person.hasLocation = true; - this.person.hasBirthLocation = true; - } - if (profileObj.DeathLocation != null && profileObj.DeathLocation.length > 0) { - this.person.hasLocation = true; - this.person.hasDeathLocation = true; - } - - // note that DataStatus might not be returned, depending on privacy - if (profileObj.DataStatus != null) { - // To check for DNA status use Bio parent if not null else default to just parent - if ((profileObj.DataStatus.BioFather == BioCheckPerson.CONF_WITH_DNA_STATUS) || - (profileObj.DataStatus.Father == BioCheckPerson.CONF_WITH_DNA_STATUS)) { - this.person.fatherDnaConfirmed = true; + if (profileObj.IsMember != null) { + if (profileObj.IsMember === 1) { + this.person.isMember = true; + } } - if ((profileObj.DataStatus.BioMother == BioCheckPerson.CONF_WITH_DNA_STATUS) || - (profileObj.DataStatus.Mother == BioCheckPerson.CONF_WITH_DNA_STATUS)) { - this.person.motherDnaConfirmed = true; + if (profileObj.FirstName != null) { + this.person.firstName = profileObj.FirstName; + } else { + if (profileObj.RealName != null) { + this.person.firstName = profileObj.RealName; + } } - } - // can use if logged in user is the same as Manager - if (this.person.privacyLevel < BioCheckPerson.MIN_PRIVACY) { - if (userId === 0) { - canUseThis = false; // user not logged in + if (profileObj.LastNameCurrent != null) { + this.person.lastName = profileObj.LastNameCurrent; } else { - if (this.person.managerId !== userId) { - canUseThis = false; + if (profileObj.LastNameAtBirth != null) { + this.person.lastName = profileObj.LastNameAtBirth; } } - } - if (profileObj.bio == null) { - canUseThis = false; - } - if (profileObj.Manager !== null && profileObj.Manager === 0) { - this.person.isOrphan = true; - } - if (mustBeOrphan && !this.person.isOrphan) { - canUseThis = false; - } - - // Do not check the profile for a member - // TODO not sure that you want to do this, need team guidance - /* - if (this.person.isMember) { - canUseThis = false; - } - */ - if (mustBeOpen && this.person.privacyLevel < BioCheckPerson.OPEN_PRIVACY) { - canUseThis = false; - } - if (!canUseThis) { - this.person.uncheckedDueToPrivacy = true; - } else { - // check for birth/death date before 1500 - if (ignorePre1500 && this.#isPre1500) { + if (profileObj.BirthLocation != null && profileObj.BirthLocation.length > 0) { + this.person.hasLocation = true; + this.person.hasBirthLocation = true; + } + if (profileObj.DeathLocation != null && profileObj.DeathLocation.length > 0) { + this.person.hasLocation = true; + this.person.hasDeathLocation = true; + } + + // note that DataStatus might not be returned, depending on privacy + if (profileObj.DataStatus != null) { + // To check for DNA status use Bio parent if not null else default to just parent + if ((profileObj.DataStatus.BioFather == BioCheckPerson.CONF_WITH_DNA_STATUS) || + (profileObj.DataStatus.Father == BioCheckPerson.CONF_WITH_DNA_STATUS)) { + this.person.fatherDnaConfirmed = true; + } + if ((profileObj.DataStatus.BioMother == BioCheckPerson.CONF_WITH_DNA_STATUS) || + (profileObj.DataStatus.Mother == BioCheckPerson.CONF_WITH_DNA_STATUS)) { + this.person.motherDnaConfirmed = true; + } + } + // can use if logged in user is the same as Manager + if (this.person.privacyLevel < BioCheckPerson.MIN_PRIVACY) { + if (userId === 0) { + canUseThis = false; // user not logged in + } else { + if (this.person.managerId !== userId) { + canUseThis = false; + } + } + } + if (profileObj.bio == null) { canUseThis = false; - this.person.uncheckedDueToDate = true; } - } - // Don't bother with REDIRECT unless you can use the profile anyway - if (canUseThis && profileObj.bio != null) { - this.person.bio = profileObj.bio; - this.person.hasBio = true; - // TODO this is a HACK - // to see if resolveRedirect was not honored by the API - // look for a bio content that starts with - // and if so set hasBio false to force a call to the getBio API - // Note that this might happen for check Watchlist, since the - // API does not honor resolveRedirect as of Dec 2025 - if (profileObj.bio.startsWith("#REDIRECT")) { - console.log("BioCheck biography starts with #REDIRECT for profile Id " + profileObj.Id); - this.person.hasBio = false; + if (profileObj.Manager !== null && profileObj.Manager === 0) { + this.person.isOrphan = true; + } + if (mustBeOrphan && !this.person.isOrphan) { + canUseThis = false; + } + + if (profileObj.Managers != null) { + for (let i=0; ixxx pairs let bioLineString = this.#getBioLineString(); this.#findRef(bioLineString); @@ -1629,6 +1636,12 @@ validateSourcesStr(sourcesStr, thePerson) { this.#bioScore--; this.#messages.sectionMessages.push('Too many Stickers: ' + this.#stats.numberStickers); } + if (this.#style.bioMissingLocations) { + this.#style.bioHasStyleIssues = true; + this.#bioScore = this.#bioScore - 5; + this.#messages.sectionMessages.push('Profile has neither birth nor death location'); + } + if (this.#style.acknowledgementsHeadingHasExtraEqual) { this.#style.bioHasStyleIssues = true; this.#messages.sectionMessages.push('Acknowledgements subsection instead of section'); diff --git a/src/features/bioCheck/README.md b/src/features/bioCheck/README.md index 70a5cf40..29227c09 100644 --- a/src/features/bioCheck/README.md +++ b/src/features/bioCheck/README.md @@ -65,8 +65,8 @@ Only contains a subset of the complete set of data available. Expects the profile to contain the following fields from the API: Id,Name,IsLiving,Privacy,Manager,IsMember, BirthDate,DeathDate,BirthDateDecade,DeathDateDecade, -BirthLocation,DeathLocation, -FirstName,RealName,LastNameCurrent,LastNameAtBirth,Mother,Father,DataStatus,Bio +BirthLocation,DeathLocation,Managers, +FirstName,RealName,LastNameCurrent,LastNameAtBirth,DataStatus,Bio ### canUse @@ -138,6 +138,12 @@ Is profile an orphan Returns **[Boolean][7]** true if profile is an orphan +### getManagers + +Get managers for this profile as a String + +Returns **[String][8]** list of managers + ### hasLocation Does profile have either birth or death location @@ -156,30 +162,6 @@ Does profile have death location Returns **[Boolean][7]** true if death location -### hasFather - -Does profile have father - -Returns **[Boolean][7]** true if profile has father - -### hasMother - -Does profile have mother - -Returns **[Boolean][7]** true if profile has mother - -### hasFatherStatus - -Does profile have father status - -Returns **[Boolean][7]** true if profile has father status - -### hasMotherStatus - -Does profile have mother status - -Returns **[Boolean][7]** true if profile has mother status - ### getPrivacy Get the privacy @@ -755,6 +737,18 @@ assumes the leading {{ removed and line is lower case Returns **[Boolean][7]** true if nav box else false +### isNotabilityTemplate + +Determine if line is a Notability template +Do not want to look at all formatting templates, at present +assumes the leading {{ removed and line is lower case + +#### Parameters + +* `line` **[String][8]** to test + +Returns **[Boolean][7]** true if notability else false + ### isSticker Determine if a line is a valid Sticker From 75caf69a831adf245f7218e320af71c6d3030810 Mon Sep 17 00:00:00 2001 From: ke4tch Date: Wed, 29 Apr 2026 13:09:07 -0400 Subject: [PATCH 6/7] Do not report missing location or date if hidden by privacy --- src/features/bioCheck/BioCheckPerson.js | 16 ++++++++++++---- src/features/bioCheck/README.md | 2 ++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/features/bioCheck/BioCheckPerson.js b/src/features/bioCheck/BioCheckPerson.js index 2aecca11..2a0d5803 100644 --- a/src/features/bioCheck/BioCheckPerson.js +++ b/src/features/bioCheck/BioCheckPerson.js @@ -339,9 +339,14 @@ export class BioCheckPerson { /** * Does profile have either birth or death location * @returns {Boolean} true if either location present + * or the privacy does not let us determine location */ hasLocation() { - return this.person.hasLocation; + if (this.person.privacyLevel >= BioCheckPerson.MIN_PRIVACY) { + return true; + } else { + return this.person.hasLocation; + } } /** * Does profile have birth location @@ -680,17 +685,20 @@ export class BioCheckPerson { } /** * Does the profile lack dates + * Only looks at open and private profiles * @returns {Boolean} true if profile has neither birth nor death date */ isUndated() { + let undated = false; if (!this.#hasBirthDate && !this.#hasDeathDate) { this.#isPre1500 = true; this.#isPre1700 = true; this.#tooOldToRemember = true; - return true; - } else { - return false; + if (this.person.privacyLevel > BioCheckPerson.MIN_PRIVACY) { + undated = true; + } } + return undated; } /** diff --git a/src/features/bioCheck/README.md b/src/features/bioCheck/README.md index 29227c09..531b9ace 100644 --- a/src/features/bioCheck/README.md +++ b/src/features/bioCheck/README.md @@ -149,6 +149,7 @@ Returns **[String][8]** list of managers Does profile have either birth or death location Returns **[Boolean][7]** true if either location present +or the privacy does not let us determine location ### hasBirthLocation @@ -222,6 +223,7 @@ Does the profile have a death date ### isUndated Does the profile lack dates +Only looks at open and private profiles Returns **[Boolean][7]** true if profile has neither birth nor death date From ac0ad3dc586f0c8afff8d65fc1286d92022cb1d0 Mon Sep 17 00:00:00 2001 From: ke4tch Date: Fri, 22 May 2026 12:32:31 -0400 Subject: [PATCH 7/7] Sync with changes to support Research Status (not used in WBE version yet) --- src/features/bioCheck/BioCheckPerson.js | 48 +++++++++++++++++++++++++ src/features/bioCheck/README.md | 12 +++++++ src/features/bioCheck/SourceRules.js | 4 +-- 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/features/bioCheck/BioCheckPerson.js b/src/features/bioCheck/BioCheckPerson.js index 2a0d5803..e1cd0795 100644 --- a/src/features/bioCheck/BioCheckPerson.js +++ b/src/features/bioCheck/BioCheckPerson.js @@ -65,6 +65,7 @@ export class BioCheckPerson { bio: "", hasName: false, privacyLevel: 0, + researchStatus: 0, isMember: false, isOrphan: false, hasLocation: false, @@ -188,6 +189,12 @@ export class BioCheckPerson { this.person.motherDnaConfirmed = true; } } + + // Populate researchStatus from returned value + if (profileObj.ResearchStatus != null) { + this.person.researchStatus = profileObj.ResearchStatus; + } + // can use if logged in user is the same as Manager if (this.person.privacyLevel < BioCheckPerson.MIN_PRIVACY) { if (userId === 0) { @@ -403,6 +410,45 @@ export class BioCheckPerson { } return privacyString; } + /** + * Get the research status + * @returns {Number} numeric research status + */ + getResearchStatus() { + return this.person.researchStatus; + } + /** + * Get the research status as a string to be displayed to the user + * @returns {String} research status string + */ + getResearchStatusString() { + let researchString = ""; + switch (this.person.researchStatus) { + case 0: + researchString = " "; // No status + break; + case 10: // Unfinished + researchString = "Unfinished"; + break; + case 20: // Help Requested + researchString = "Help Requested"; + break; + case 30: // Sources to Review + researchString = "Sources to Review"; + break; + case 40: // Silver Standard + researchString = "Silver Research"; + break; + case 50: // Gold Standard Candidate + researchString = "Pending Review"; + break; + case 60: // Gold Standard: Genealogically Complete and Peer Reviewed + researchString = "Peer Reviewed "; + break; + } + return researchString; + } + /** * Was profile not checked due to privacy * @returns {Boolean} true if profile could not be checked due to privacy @@ -511,6 +557,8 @@ export class BioCheckPerson { if (emailElements.length > 0) { this.person.isMember = true; } + + // TODO set researchStatus } /* diff --git a/src/features/bioCheck/README.md b/src/features/bioCheck/README.md index 531b9ace..cb677682 100644 --- a/src/features/bioCheck/README.md +++ b/src/features/bioCheck/README.md @@ -175,6 +175,18 @@ Get the privacy as a string to be displayed to the user Returns **[String][8]** privacy string (i.e., the color) +### getResearchStatus + +Get the research status + +Returns **[Number][9]** numeric research status + +### getResearchStatusString + +Get the research status as a string to be displayed to the user + +Returns **[String][8]** research status string + ### isUncheckedDueToPrivacy Was profile not checked due to privacy diff --git a/src/features/bioCheck/SourceRules.js b/src/features/bioCheck/SourceRules.js index a30c0620..3212b4f3 100644 --- a/src/features/bioCheck/SourceRules.js +++ b/src/features/bioCheck/SourceRules.js @@ -1001,9 +1001,7 @@ class SourceRules { projectBox.status = templates[i].status.toLowerCase().trim(); this.#projectBox.push(projectBox); } - // TODO hack that Notability may be set to the wrong type - if ((templates[i].type.toLowerCase().trim() === 'formatting') || - (templates[i].type.toLowerCase().trim() === 'formattingtemplate')) { + if (templates[i].type.toLowerCase().trim() === 'formattingtemplate') { let formattingTemplate = { name: "", status: "",