Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
55 changes: 52 additions & 3 deletions dingo/model/rule/scibase/rule_quanliang.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@
OA_BOOL_VALUES = {"true", "false", "unknown"}
METADATA_TYPE_VALUES = {"paper", "ebook"}
OA_STATUS_VALUES = {"diamond", "gold", "green", "hybrid", "bronze", "closed", ""}
OPEN_OA_STATUS_VALUES = {"diamond", "gold", "green", "hybrid", "bronze"}
LOC_TYPE_VALUES = {"download", "reader", "display", ""}
JSON_LIST_FIELDS = {
"isbns",
Expand Down Expand Up @@ -1339,7 +1340,13 @@ def check_locations(locations: Any) -> ValidationResult:
return _ok()


def check_access_is_oa(access_is_oa: Any, metadata_type: Any) -> ValidationResult:
def check_access_is_oa(
access_is_oa: Any,
metadata_type: Any,
access_oa_status: Any = None,
access_oa_url: Any = None,
access_license: Any = None,
) -> ValidationResult:
if metadata_type not in METADATA_TYPE_VALUES:
return _ok()
required = metadata_type == "paper"
Expand All @@ -1351,7 +1358,43 @@ def check_access_is_oa(access_is_oa: Any, metadata_type: Any) -> ValidationResul
return _fail("empty", "value cannot be empty string when metadata_type='paper'") if required else _ok()
if access_is_oa not in OA_BOOL_VALUES:
return _fail("unsupported_value", f"unsupported value '{access_is_oa}'")
return _ok()

error_labels: list[str] = []
reasons: list[str] = []

# Only apply consistency rules to otherwise valid dependent values. Their
# own validators report null, type, enum and URL-format errors separately.
status_is_valid = isinstance(access_oa_status, str) and access_oa_status in OA_STATUS_VALUES
url_is_string_list = isinstance(access_oa_url, list) and all(
isinstance(item, str) for item in access_oa_url
)
license_is_valid = isinstance(access_license, str) and access_license in ACCESS_LICENSE_VALUES

if access_is_oa == "true":
if status_is_valid and access_oa_status not in OPEN_OA_STATUS_VALUES:
error_labels.append("oa_status_mismatch")
reasons.append(
"access_oa_status must be one of diamond, gold, green, hybrid or bronze "
"when access_is_oa='true'"
)
if url_is_string_list and not any(URL_RE.fullmatch(item) for item in access_oa_url):
error_labels.append("oa_url_mismatch")
reasons.append("access_oa_url must contain at least one valid URL when access_is_oa='true'")
else:
expected_status = "closed" if access_is_oa == "false" else ""
if status_is_valid and access_oa_status != expected_status:
error_labels.append("oa_status_mismatch")
reasons.append(
f"access_oa_status must be '{expected_status}' when access_is_oa='{access_is_oa}'"
)
if url_is_string_list and access_oa_url != []:
error_labels.append("oa_url_mismatch")
reasons.append(f"access_oa_url must be empty when access_is_oa='{access_is_oa}'")
if license_is_valid and access_license != "":
error_labels.append("license_mismatch")
reasons.append(f"access_license must be empty when access_is_oa='{access_is_oa}'")

return bool(error_labels), error_labels, reasons


def check_access_oa_status(access_oa_status: Any) -> ValidationResult:
Expand Down Expand Up @@ -1772,7 +1815,13 @@ def normalize_record(record: Dict[str, Any]) -> Dict[str, Any]:
"author": lambda record: check_author(record.get("author")),
"contributors": lambda record: check_contributors(record.get("contributors")),
"locations": lambda record: check_locations(record.get("locations")),
"access_is_oa": lambda record: check_access_is_oa(record.get("access_is_oa"), record.get("metadata_type")),
"access_is_oa": lambda record: check_access_is_oa(
record.get("access_is_oa"),
record.get("metadata_type"),
record.get("access_oa_status"),
record.get("access_oa_url"),
record.get("access_license"),
),
"access_oa_status": lambda record: check_access_oa_status(record.get("access_oa_status")),
"access_oa_url": lambda record: check_access_oa_url(record.get("access_oa_url")),
"access_license": lambda record: check_access_license(record.get("access_license")),
Expand Down
5 changes: 4 additions & 1 deletion docs/scibase/rule_quanliang_labels.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
| `author` | 10 |
| `contributors` | 4 |
| `locations` | 6 |
| `access_is_oa` | 4 |
| `access_is_oa` | 7 |
| `access_oa_status` | 4 |
| `access_oa_url` | 4 |
| `access_license` | 4 |
Expand Down Expand Up @@ -225,6 +225,9 @@
| `wrong_type` | 字段类型错误 | 值的类型不是字符串时标记。 |
| `empty` | 值为空 | 若论文开放获取标记为空,则标记。 |
| `unsupported_value` | 值不在限定范围内 | 值须属于 3 个限定值之一:`true`、`false`、`unknown`。 |
| `oa_status_mismatch` | OA 状态不一致 | `true` 时状态须为 `diamond`、`gold`、`green`、`hybrid` 或 `bronze`;`false` 时须为 `closed`;`unknown` 时须为空。 |
| `oa_url_mismatch` | OA URL 不一致 | `true` 时须至少包含一个有效 OA URL;`false` 或 `unknown` 时须为空数组。 |
| `license_mismatch` | 许可证不一致 | `false` 或 `unknown` 时 `access_license` 须为空;`true` 时允许为空或使用受支持的许可证值。 |

## access_oa_status

Expand Down
126 changes: 126 additions & 0 deletions test/scripts/model/rule/test_rule_scibase.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,132 @@


class TestRuleQuanliangFieldValidation:
def test_access_oa_consistency_valid_combinations(self):
cases = [
{
"access_is_oa": "true",
"access_oa_status": "gold",
"access_oa_url": ["https://example.com/paper.pdf"],
"access_license": "",
},
{
"access_is_oa": "true",
"access_oa_status": "green",
"access_oa_url": ["https://example.com/paper.pdf"],
"access_license": "cc-by",
},
{
"access_is_oa": "false",
"access_oa_status": "closed",
"access_oa_url": [],
"access_license": "",
},
{
"access_is_oa": "unknown",
"access_oa_status": "",
"access_oa_url": [],
"access_license": "",
},
]

for case in cases:
model = RuleQuanliangFieldValidation()
model.dynamic_config = model.dynamic_config.model_copy(deep=True)
model.dynamic_config.key_list = [
"access_is_oa",
"access_oa_status",
"access_oa_url",
"access_license",
]

result = model.eval(Data(metadata_type="paper", **case))

assert result.status is False, case
assert result.label == ["QUALITY_GOOD"], case

def test_access_oa_consistency_reports_all_mismatches(self):
cases = [
(
{
"access_is_oa": "true",
"access_oa_status": "closed",
"access_oa_url": [],
"access_license": "",
},
[
"access_is_oa.oa_status_mismatch",
"access_is_oa.oa_url_mismatch",
],
),
(
{
"access_is_oa": "false",
"access_oa_status": "gold",
"access_oa_url": ["https://example.com/paper.pdf"],
"access_license": "cc-by",
},
[
"access_is_oa.oa_status_mismatch",
"access_is_oa.oa_url_mismatch",
"access_is_oa.license_mismatch",
],
),
(
{
"access_is_oa": "unknown",
"access_oa_status": "closed",
"access_oa_url": ["https://example.com/paper.pdf"],
"access_license": "mit",
},
[
"access_is_oa.oa_status_mismatch",
"access_is_oa.oa_url_mismatch",
"access_is_oa.license_mismatch",
],
),
]

for case, expected_labels in cases:
model = RuleQuanliangFieldValidation()
model.dynamic_config = model.dynamic_config.model_copy(deep=True)
model.dynamic_config.key_list = [
"access_is_oa",
"access_oa_status",
"access_oa_url",
"access_license",
]

result = model.eval(Data(metadata_type="paper", **case))

assert result.status is True, case
assert result.label == expected_labels, case

def test_access_oa_consistency_does_not_duplicate_basic_field_errors(self):
model = RuleQuanliangFieldValidation()
model.dynamic_config = model.dynamic_config.model_copy(deep=True)
model.dynamic_config.key_list = [
"access_is_oa",
"access_oa_status",
"access_oa_url",
"access_license",
]

result = model.eval(
Data(
metadata_type="paper",
access_is_oa="false",
access_oa_status=None,
access_oa_url="not-a-list",
access_license="not-a-license",
)
)

assert result.label == [
"access_oa_status.null",
"access_oa_url.wrong_type",
"access_license.unsupported_value",
]

def test_author_quality_labels(self):
model = RuleQuanliangFieldValidation()
model.dynamic_config = model.dynamic_config.model_copy(deep=True)
Expand Down
Loading