mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 06:34:54 +08:00
fix(glossary): 补齐术语删除和审核门禁
This commit is contained in:
@@ -74,9 +74,9 @@ mod translation_query;
|
||||
#[path = "workflow_commands.rs"]
|
||||
mod workflow_commands;
|
||||
use glossary_query::{
|
||||
glossary_diagnose_rpc_report, glossary_mutation_rpc_report, glossary_query_rpc_report,
|
||||
glossary_review_rpc_report, glossary_rpc_envelope, glossary_summary_rpc_report,
|
||||
run_glossary_command,
|
||||
glossary_delete_rpc_report, glossary_diagnose_rpc_report, glossary_mutation_rpc_report,
|
||||
glossary_query_rpc_report, glossary_review_rpc_report, glossary_rpc_envelope,
|
||||
glossary_summary_rpc_report, run_glossary_command,
|
||||
};
|
||||
use patch_commands::{
|
||||
is_write_patch_command, run_write_patch_command, validate_write_patch_options,
|
||||
@@ -243,6 +243,7 @@ fn run() -> anyhow::Result<i32> {
|
||||
| CliCommand::GlossaryUpdate
|
||||
| CliCommand::GlossaryApprove
|
||||
| CliCommand::GlossaryDeprecate
|
||||
| CliCommand::GlossaryDelete
|
||||
| CliCommand::GlossaryDiagnose => {
|
||||
run_glossary_command(&options)?;
|
||||
Ok(0)
|
||||
@@ -658,6 +659,7 @@ enum CliCommand {
|
||||
GlossaryUpdate,
|
||||
GlossaryApprove,
|
||||
GlossaryDeprecate,
|
||||
GlossaryDelete,
|
||||
GlossaryDiagnose,
|
||||
Repack,
|
||||
PublishLocalized,
|
||||
@@ -1182,6 +1184,7 @@ const RPC_METHOD_GLOSSARY_ADD: &str = "translation.glossary.add";
|
||||
const RPC_METHOD_GLOSSARY_UPDATE: &str = "translation.glossary.update";
|
||||
const RPC_METHOD_GLOSSARY_APPROVE: &str = "translation.glossary.approve";
|
||||
const RPC_METHOD_GLOSSARY_DEPRECATE: &str = "translation.glossary.deprecate";
|
||||
const RPC_METHOD_GLOSSARY_DELETE: &str = "translation.glossary.delete";
|
||||
const RPC_METHOD_GLOSSARY_DIAGNOSE: &str = "translation.glossary.diagnose";
|
||||
const RPC_METHOD_LOCALIZED_STATUS: &str = "localized.status";
|
||||
const RPC_METHOD_LOCALIZED_PUBLISH: &str = "localized.publish";
|
||||
@@ -2296,6 +2299,15 @@ fn dispatch_rpc_method(
|
||||
},
|
||||
),
|
||||
),
|
||||
RPC_METHOD_GLOSSARY_DELETE => glossary_rpc_envelope(
|
||||
request_id,
|
||||
glossary_delete_rpc_report(
|
||||
state_dir,
|
||||
&tasks.base_config.output_root,
|
||||
tasks.translation_worker_config.glossary_path.as_deref(),
|
||||
request.params.as_ref(),
|
||||
),
|
||||
),
|
||||
RPC_METHOD_TRANSLATION_WORKER_RUN => {
|
||||
let config = match rpc_translation_worker_config_with_defaults(
|
||||
request.params.as_ref(),
|
||||
@@ -7474,6 +7486,7 @@ fn parse_args_with_env(
|
||||
| CliCommand::GlossaryUpdate
|
||||
| CliCommand::GlossaryApprove
|
||||
| CliCommand::GlossaryDeprecate
|
||||
| CliCommand::GlossaryDelete
|
||||
| CliCommand::GlossaryDiagnose
|
||||
| CliCommand::TranslationSet
|
||||
| CliCommand::TranslationValidate
|
||||
@@ -7629,6 +7642,7 @@ fn parse_args_with_env(
|
||||
| CliCommand::GlossaryUpdate
|
||||
| CliCommand::GlossaryApprove
|
||||
| CliCommand::GlossaryDeprecate
|
||||
| CliCommand::GlossaryDelete
|
||||
| CliCommand::GlossaryDiagnose => {
|
||||
if options.watch
|
||||
|| options.daemon
|
||||
@@ -7686,6 +7700,16 @@ fn parse_args_with_env(
|
||||
));
|
||||
}
|
||||
}
|
||||
CliCommand::GlossaryDelete => {
|
||||
if options.glossary_term_id.is_none()
|
||||
|| options.glossary_reviewer.is_none()
|
||||
|| options.glossary_reason.is_none()
|
||||
{
|
||||
return Err(anyhow::anyhow!(
|
||||
"Glossary delete 必须指定 --glossary-term-id、--glossary-reviewer 和 --glossary-reason"
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
options.progress = false;
|
||||
@@ -8293,6 +8317,7 @@ fn parse_translation_glossary_command(
|
||||
"update" | "edit" => CliCommand::GlossaryUpdate,
|
||||
"approve" | "trust" => CliCommand::GlossaryApprove,
|
||||
"deprecate" | "retire" => CliCommand::GlossaryDeprecate,
|
||||
"delete" | "remove" => CliCommand::GlossaryDelete,
|
||||
"diagnose" | "check" => CliCommand::GlossaryDiagnose,
|
||||
other => {
|
||||
return Err(anyhow::anyhow!(
|
||||
|
||||
@@ -425,6 +425,20 @@ fn glossary_commands_parse_and_validate() {
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(diagnose.command, CliCommand::GlossaryDiagnose);
|
||||
let delete = parse(&[
|
||||
"bat",
|
||||
"i18n",
|
||||
"glossary",
|
||||
"delete",
|
||||
"--glossary-term-id",
|
||||
"term-sensei",
|
||||
"--glossary-reviewer",
|
||||
"operator",
|
||||
"--glossary-reason",
|
||||
"duplicate",
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(delete.command, CliCommand::GlossaryDelete);
|
||||
assert!(parse(&["bat", "i18n", "glossary", "diagnose"]).is_err());
|
||||
assert!(parse(&[
|
||||
"bat",
|
||||
@@ -2784,6 +2798,79 @@ fn dispatch_glossary_summary_reports_missing_database_without_creating_it() {
|
||||
assert!(!output_root.join("glossary.sqlite").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dispatch_glossary_delete_removes_term_and_returns_snapshot() {
|
||||
let temp = tempfile::TempDir::new().unwrap();
|
||||
let output_root = temp.path().join("output");
|
||||
let state_dir = temp.path().join("state");
|
||||
let glossary_path = output_root.join("glossary.sqlite");
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
runtime.block_on(async {
|
||||
let repository = bat_infrastructure::SqliteGlossaryRepository::new(&glossary_path)
|
||||
.await
|
||||
.unwrap();
|
||||
repository
|
||||
.add(bat_core::domain::GlossaryTermDraft {
|
||||
term_id: "term-sensei".to_string(),
|
||||
definition: bat_core::domain::GlossaryTermSnapshot {
|
||||
source_term: "Sensei".to_string(),
|
||||
aliases: vec!["Teacher".to_string()],
|
||||
recommended_translation: "老师".to_string(),
|
||||
allowed_translations: Vec::new(),
|
||||
source_language: None,
|
||||
target_language: None,
|
||||
category: Some("person".to_string()),
|
||||
priority: 10,
|
||||
scope: Default::default(),
|
||||
},
|
||||
review_status: bat_core::domain::GlossaryReviewStatus::Draft,
|
||||
source: bat_core::domain::GlossarySourceRecord {
|
||||
source_kind: bat_core::domain::GlossarySourceKind::Manual,
|
||||
source_ref: Some("test".to_string()),
|
||||
source_author: Some("tester".to_string()),
|
||||
source_note: None,
|
||||
observed_unix_seconds: 100,
|
||||
},
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
let context = test_task_context_with_config(OfficialUpdateConfig {
|
||||
output_root: output_root.clone(),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let envelope = dispatch_rpc_method(
|
||||
&rpc_request(
|
||||
"translation.glossary.delete",
|
||||
Some(serde_json::json!({
|
||||
"term_id": "term-sensei",
|
||||
"reviewer": "reviewer",
|
||||
"reason": "duplicate"
|
||||
})),
|
||||
),
|
||||
&state_dir,
|
||||
&new_daemon_control(),
|
||||
&context,
|
||||
"req-glossary-delete-1".to_string(),
|
||||
);
|
||||
let value = serde_json::to_value(envelope).unwrap();
|
||||
assert_eq!(value["ok"], true);
|
||||
assert_eq!(value["data"]["deleted"], true);
|
||||
assert_eq!(value["data"]["term"]["term_id"], "term-sensei");
|
||||
assert_eq!(value["data"]["term"]["source_term"], "Sensei");
|
||||
|
||||
runtime.block_on(async {
|
||||
let repository = bat_infrastructure::SqliteGlossaryRepository::open(&glossary_path)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(repository.find("term-sensei").await.is_err());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dispatch_translation_memory_rejects_invalid_params_with_stable_error_code() {
|
||||
let temp = tempfile::TempDir::new().unwrap();
|
||||
|
||||
@@ -65,6 +65,19 @@ pub(super) fn run_glossary_command(options: &CliOptions) -> anyhow::Result<()> {
|
||||
options.glossary_reason.clone(),
|
||||
)?
|
||||
}
|
||||
CliCommand::GlossaryDelete => {
|
||||
let term_id = options.glossary_term_id.as_deref().ok_or_else(|| {
|
||||
anyhow::anyhow!("Glossary delete 必须指定 --glossary-term-id")
|
||||
})?;
|
||||
let reviewer = options.glossary_reviewer.as_deref().ok_or_else(|| {
|
||||
anyhow::anyhow!("Glossary delete 必须指定 --glossary-reviewer")
|
||||
})?;
|
||||
let reason = options
|
||||
.glossary_reason
|
||||
.as_deref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Glossary delete 必须指定 --glossary-reason"))?;
|
||||
build_glossary_delete_report(&path, term_id, reviewer, reason)?
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
print_json_value(options.output_format, &report)
|
||||
@@ -78,6 +91,7 @@ fn glossary_method(command: CliCommand) -> anyhow::Result<&'static str> {
|
||||
CliCommand::GlossaryUpdate => RPC_METHOD_GLOSSARY_UPDATE,
|
||||
CliCommand::GlossaryApprove => RPC_METHOD_GLOSSARY_APPROVE,
|
||||
CliCommand::GlossaryDeprecate => RPC_METHOD_GLOSSARY_DEPRECATE,
|
||||
CliCommand::GlossaryDelete => RPC_METHOD_GLOSSARY_DELETE,
|
||||
CliCommand::GlossaryDiagnose => RPC_METHOD_GLOSSARY_DIAGNOSE,
|
||||
_ => return Err(anyhow::anyhow!("不是 Glossary 命令")),
|
||||
})
|
||||
@@ -159,6 +173,20 @@ fn glossary_cli_params(options: &CliOptions) -> anyhow::Result<Option<serde_json
|
||||
params.insert("reason".to_string(), serde_json::json!(reason));
|
||||
}
|
||||
}
|
||||
CliCommand::GlossaryDelete => {
|
||||
params.insert(
|
||||
"term_id".to_string(),
|
||||
serde_json::json!(options.glossary_term_id.as_deref().unwrap_or_default()),
|
||||
);
|
||||
params.insert(
|
||||
"reviewer".to_string(),
|
||||
serde_json::json!(options.glossary_reviewer.as_deref().unwrap_or_default()),
|
||||
);
|
||||
params.insert(
|
||||
"reason".to_string(),
|
||||
serde_json::json!(options.glossary_reason.as_deref().unwrap_or_default()),
|
||||
);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
Ok(Some(serde_json::Value::Object(params)))
|
||||
@@ -414,6 +442,33 @@ pub(super) fn build_glossary_review_report(
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) fn build_glossary_delete_report(
|
||||
path: &std::path::Path,
|
||||
term_id: &str,
|
||||
reviewer: &str,
|
||||
reason: &str,
|
||||
) -> anyhow::Result<serde_json::Value> {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()?;
|
||||
let term = runtime.block_on(async {
|
||||
let repository = SqliteGlossaryRepository::open(path)
|
||||
.await
|
||||
.map_err(|error| anyhow::anyhow!("{error}"))?;
|
||||
repository
|
||||
.delete(term_id, reviewer, reason)
|
||||
.await
|
||||
.map_err(|error| anyhow::anyhow!("{error}"))
|
||||
})?;
|
||||
Ok(serde_json::json!({
|
||||
"available": true,
|
||||
"path": path,
|
||||
"schema_version": GLOSSARY_SCHEMA_VERSION,
|
||||
"deleted": true,
|
||||
"term": term,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) fn glossary_rpc_envelope(
|
||||
request_id: String,
|
||||
result: Result<serde_json::Value, ApiError>,
|
||||
@@ -546,6 +601,30 @@ pub(super) fn glossary_review_rpc_report(
|
||||
.map_err(|error| glossary_internal_error(method, error))
|
||||
}
|
||||
|
||||
pub(super) fn glossary_delete_rpc_report(
|
||||
state_dir: &Path,
|
||||
output_root: &Path,
|
||||
default_path: Option<&Path>,
|
||||
params: Option<&serde_json::Value>,
|
||||
) -> Result<serde_json::Value, ApiError> {
|
||||
let params = glossary_params(params, RPC_METHOD_GLOSSARY_DELETE)?;
|
||||
let term_id = glossary_string(¶ms, "term_id", RPC_METHOD_GLOSSARY_DELETE)?
|
||||
.ok_or_else(|| glossary_invalid(RPC_METHOD_GLOSSARY_DELETE, "缺少 term_id"))?;
|
||||
let reviewer = glossary_string(¶ms, "reviewer", RPC_METHOD_GLOSSARY_DELETE)?
|
||||
.ok_or_else(|| glossary_invalid(RPC_METHOD_GLOSSARY_DELETE, "缺少 reviewer"))?;
|
||||
let reason = glossary_string(¶ms, "reason", RPC_METHOD_GLOSSARY_DELETE)?
|
||||
.ok_or_else(|| glossary_invalid(RPC_METHOD_GLOSSARY_DELETE, "缺少 reason"))?;
|
||||
let path = glossary_rpc_path(
|
||||
state_dir,
|
||||
output_root,
|
||||
default_path,
|
||||
Some(&serde_json::Value::Object(params.clone())),
|
||||
RPC_METHOD_GLOSSARY_DELETE,
|
||||
)?;
|
||||
build_glossary_delete_report(&path, term_id, reviewer, reason)
|
||||
.map_err(|error| glossary_internal_error(RPC_METHOD_GLOSSARY_DELETE, error))
|
||||
}
|
||||
|
||||
fn glossary_rpc_path(
|
||||
state_dir: &Path,
|
||||
output_root: &Path,
|
||||
|
||||
@@ -348,6 +348,7 @@ Commands:
|
||||
i18n glossary summary/query Show project Glossary terms and review counts
|
||||
i18n glossary add/update Add or replace one Glossary term definition
|
||||
i18n glossary approve/deprecate Review one Glossary term
|
||||
i18n glossary delete Remove one Glossary term with reviewer and reason
|
||||
i18n glossary diagnose Run deterministic Glossary QA for one TextUnit source
|
||||
i18n publish Publish a localized release from a workbench or worker results
|
||||
i18n rollback Roll back the current localized release
|
||||
@@ -439,13 +440,13 @@ Sync:
|
||||
--translation-provider <NAME> / --provider <NAME> Provider for i18n worker run (mock/crowdin)
|
||||
--translation-fixture <PATH> Mock/provider fixture for i18n worker run
|
||||
--glossary-path <PATH> Project Glossary SQLite path
|
||||
--glossary-term-id <ID> Glossary term ID for add/update/review
|
||||
--glossary-term-id <ID> Glossary term ID for add/update/review/delete
|
||||
--glossary-source-term <TEXT> Source spelling for a Glossary term
|
||||
--glossary-recommended-translation <TEXT> Recommended target translation
|
||||
--glossary-source-text <TEXT> Source TextUnit text for Glossary query/diagnose
|
||||
--glossary-context-json <JSON> TextUnit context for Glossary diagnose
|
||||
--glossary-reviewer <ID> Reviewer for Glossary updates/reviews
|
||||
--glossary-reason <TEXT> Reason for Glossary review or override
|
||||
--glossary-reviewer <ID> Reviewer for Glossary updates/reviews/delete
|
||||
--glossary-reason <TEXT> Reason for Glossary review/delete or override
|
||||
--glossary-provenance <TEXT> Provenance for an explicit Glossary override
|
||||
--worker-concurrency <N> Translation worker concurrency (default: 8, range 1..=256)
|
||||
--worker-max-attempts <N> Maximum claims per translation task
|
||||
|
||||
+139
-14
@@ -160,6 +160,21 @@ impl SqliteGlossaryRepository {
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(db_error)?;
|
||||
sqlx::query(
|
||||
"CREATE TABLE IF NOT EXISTS glossary_term_deletions (
|
||||
deletion_id TEXT PRIMARY KEY NOT NULL,
|
||||
term_id TEXT NOT NULL,
|
||||
reviewer TEXT NOT NULL,
|
||||
reason TEXT NOT NULL,
|
||||
source_json TEXT NOT NULL,
|
||||
snapshot_json TEXT NOT NULL,
|
||||
history_json TEXT NOT NULL,
|
||||
observed_unix_seconds INTEGER NOT NULL
|
||||
)",
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(db_error)?;
|
||||
sqlx::query(
|
||||
"CREATE INDEX IF NOT EXISTS idx_glossary_status
|
||||
ON glossary_terms(review_status, priority DESC, term_id)",
|
||||
@@ -244,6 +259,31 @@ impl SqliteGlossaryRepository {
|
||||
"Glossary query limit 必须在 1..=1000 范围内".to_string(),
|
||||
));
|
||||
}
|
||||
let candidates = self.load_terms(category, review_status).await?;
|
||||
let mut terms = Vec::new();
|
||||
for term in candidates {
|
||||
if source_text.is_none_or(|source| {
|
||||
let mut spellings = vec![term.definition.source_term.as_str()];
|
||||
spellings.extend(term.definition.aliases.iter().map(String::as_str));
|
||||
spellings
|
||||
.into_iter()
|
||||
.filter(|spelling| !spelling.is_empty())
|
||||
.any(|spelling| source.contains(spelling))
|
||||
}) {
|
||||
terms.push(term);
|
||||
if terms.len() >= limit {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(terms)
|
||||
}
|
||||
|
||||
async fn load_terms(
|
||||
&self,
|
||||
category: Option<&str>,
|
||||
review_status: Option<GlossaryReviewStatus>,
|
||||
) -> Result<Vec<GlossaryTerm>> {
|
||||
let rows = sqlx::query(
|
||||
"SELECT term_id FROM glossary_terms
|
||||
WHERE (?1 IS NULL OR category = ?1)
|
||||
@@ -258,19 +298,7 @@ impl SqliteGlossaryRepository {
|
||||
let mut terms = Vec::new();
|
||||
for row in rows {
|
||||
let term_id: String = row.try_get("term_id").map_err(db_error)?;
|
||||
let term = self.find(&term_id).await?;
|
||||
if source_text.is_none_or(|source| {
|
||||
let mut spellings = vec![term.definition.source_term.as_str()];
|
||||
spellings.extend(term.definition.aliases.iter().map(String::as_str));
|
||||
spellings
|
||||
.into_iter()
|
||||
.any(|spelling| source.contains(spelling))
|
||||
}) {
|
||||
terms.push(term);
|
||||
if terms.len() >= limit {
|
||||
break;
|
||||
}
|
||||
}
|
||||
terms.push(self.find(&term_id).await?);
|
||||
}
|
||||
Ok(terms)
|
||||
}
|
||||
@@ -303,6 +331,11 @@ impl SqliteGlossaryRepository {
|
||||
/// Adds a term and records its source snapshot.
|
||||
pub async fn add(&self, draft: GlossaryTermDraft) -> Result<GlossaryTerm> {
|
||||
validate_glossary_draft(&draft)?;
|
||||
if draft.review_status != GlossaryReviewStatus::Draft {
|
||||
return Err(Error::InvalidArgument(
|
||||
"Glossary add 只能创建 draft;请通过 review/approve 使术语生效".to_string(),
|
||||
));
|
||||
}
|
||||
let now = draft.source.observed_unix_seconds;
|
||||
let term = term_from_draft(&draft, now, now);
|
||||
let mut transaction = self.pool.begin().await.map_err(db_error)?;
|
||||
@@ -332,6 +365,11 @@ impl SqliteGlossaryRepository {
|
||||
reason: Option<String>,
|
||||
) -> Result<GlossaryTerm> {
|
||||
validate_glossary_draft(&draft)?;
|
||||
if draft.review_status != GlossaryReviewStatus::Draft {
|
||||
return Err(Error::InvalidArgument(
|
||||
"Glossary update 只能写入 draft;修改 approved 术语后必须重新 approve".to_string(),
|
||||
));
|
||||
}
|
||||
if reviewer.trim().is_empty() {
|
||||
return Err(Error::InvalidArgument(
|
||||
"Glossary update reviewer 不能为空".to_string(),
|
||||
@@ -355,6 +393,76 @@ impl SqliteGlossaryRepository {
|
||||
self.find(&draft.term_id).await
|
||||
}
|
||||
|
||||
/// Permanently removes a term and its stored history after explicit review.
|
||||
pub async fn delete(
|
||||
&self,
|
||||
term_id: &str,
|
||||
reviewer: &str,
|
||||
reason: &str,
|
||||
) -> Result<GlossaryTerm> {
|
||||
if reviewer.trim().is_empty() {
|
||||
return Err(Error::InvalidArgument(
|
||||
"Glossary delete reviewer 不能为空".to_string(),
|
||||
));
|
||||
}
|
||||
if reason.trim().is_empty() {
|
||||
return Err(Error::InvalidArgument(
|
||||
"Glossary delete reason 不能为空".to_string(),
|
||||
));
|
||||
}
|
||||
let term = self.find(term_id).await?;
|
||||
let mut transaction = self.pool.begin().await.map_err(db_error)?;
|
||||
let source_json = json(&term.source)?;
|
||||
let snapshot_json = json(&term.definition)?;
|
||||
let history_json = json(&term.history)?;
|
||||
let observed = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
let mut deletion_hasher = blake3::Hasher::new();
|
||||
for value in [term_id, reviewer.trim(), reason.trim()] {
|
||||
deletion_hasher.update(value.as_bytes());
|
||||
deletion_hasher.update(&[0]);
|
||||
}
|
||||
deletion_hasher.update(&observed.to_le_bytes());
|
||||
deletion_hasher.update(
|
||||
&SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_nanos()
|
||||
.to_le_bytes(),
|
||||
);
|
||||
sqlx::query(
|
||||
"INSERT INTO glossary_term_deletions (
|
||||
deletion_id, term_id, reviewer, reason, source_json,
|
||||
snapshot_json, history_json, observed_unix_seconds
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
|
||||
)
|
||||
.bind(format!("gld-{}", deletion_hasher.finalize().to_hex()))
|
||||
.bind(term_id)
|
||||
.bind(reviewer.trim())
|
||||
.bind(reason.trim())
|
||||
.bind(source_json)
|
||||
.bind(snapshot_json)
|
||||
.bind(history_json)
|
||||
.bind(i64::try_from(observed).unwrap_or(i64::MAX))
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(db_error)?;
|
||||
sqlx::query("DELETE FROM glossary_term_history WHERE term_id = ?1")
|
||||
.bind(term_id)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(db_error)?;
|
||||
sqlx::query("DELETE FROM glossary_terms WHERE term_id = ?1")
|
||||
.bind(term_id)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(db_error)?;
|
||||
transaction.commit().await.map_err(db_error)?;
|
||||
Ok(term)
|
||||
}
|
||||
|
||||
/// Changes review state and records a source/review history entry.
|
||||
pub async fn review(
|
||||
&self,
|
||||
@@ -416,7 +524,7 @@ impl SqliteGlossaryRepository {
|
||||
context: &TranslationMemoryContext,
|
||||
) -> Result<GlossaryEvaluation> {
|
||||
let terms = self
|
||||
.query(None, None, Some(GlossaryReviewStatus::Approved), 1000)
|
||||
.load_terms(None, Some(GlossaryReviewStatus::Approved))
|
||||
.await?;
|
||||
Ok(evaluate_glossary(&terms, source_text, context))
|
||||
}
|
||||
@@ -723,6 +831,10 @@ mod tests {
|
||||
let repository = SqliteGlossaryRepository::new(temp.path().join("glossary.sqlite"))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(repository
|
||||
.add(draft(GlossaryReviewStatus::Approved))
|
||||
.await
|
||||
.is_err());
|
||||
repository
|
||||
.add(draft(GlossaryReviewStatus::Draft))
|
||||
.await
|
||||
@@ -753,5 +865,18 @@ mod tests {
|
||||
assert_eq!(term.history[1].action, "approved");
|
||||
let summary = repository.summary().await.unwrap();
|
||||
assert_eq!(summary.approved_count, 1);
|
||||
let deleted = repository
|
||||
.delete("term-sensei", "reviewer", "remove duplicate")
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(deleted.history.len(), 2);
|
||||
assert!(repository.find("term-sensei").await.is_err());
|
||||
assert_eq!(repository.summary().await.unwrap().term_count, 0);
|
||||
let deletion_count: i64 =
|
||||
sqlx::query_scalar("SELECT COUNT(*) FROM glossary_term_deletions")
|
||||
.fetch_one(&repository.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(deletion_count, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1660,7 +1660,7 @@ mod tests {
|
||||
priority: 10,
|
||||
scope: BTreeMap::new(),
|
||||
},
|
||||
review_status: bat_core::domain::GlossaryReviewStatus::Approved,
|
||||
review_status: bat_core::domain::GlossaryReviewStatus::Draft,
|
||||
source: bat_core::domain::GlossarySourceRecord {
|
||||
source_kind: bat_core::domain::GlossarySourceKind::Manual,
|
||||
source_ref: Some("worker-test".to_string()),
|
||||
@@ -1671,6 +1671,15 @@ mod tests {
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
glossary
|
||||
.review(
|
||||
"term-source-0",
|
||||
bat_core::domain::GlossaryReviewStatus::Approved,
|
||||
"reviewer",
|
||||
Some("test approval".to_string()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
#[derive(Debug)]
|
||||
struct GlossaryProvider {
|
||||
|
||||
Reference in New Issue
Block a user