266 lines
8.4 KiB
Rust
266 lines
8.4 KiB
Rust
//! Read-only expansion and atomic activation for one workspace.
|
|
use crate::{
|
|
domain::*,
|
|
engine,
|
|
error::{Result, ensure},
|
|
generation::{self, Issue, Rule, Variable},
|
|
providers,
|
|
store::{Store, WorkspaceRecord},
|
|
};
|
|
use chrono::{Duration, Utc};
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::{Value, json};
|
|
use uuid::Uuid;
|
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
pub struct Draft {
|
|
pub revision: i64,
|
|
#[serde(default)]
|
|
pub shared_revision: Option<i64>,
|
|
pub variables: Vec<Variable>,
|
|
pub rules: Vec<Rule>,
|
|
#[serde(default)]
|
|
pub route_position: usize,
|
|
#[serde(default)]
|
|
pub review_id: Option<String>,
|
|
}
|
|
struct Prepared {
|
|
report: Value,
|
|
record: WorkspaceRecord,
|
|
issues: Vec<Issue>,
|
|
review: String,
|
|
}
|
|
async fn prepare(db: &Store, actor: Uuid, workspace: Uuid, draft: &Draft) -> Result<Prepared> {
|
|
let mut record = db
|
|
.prepare(workspace, draft.revision, actor, Permission::EditSchedules)
|
|
.await?;
|
|
let directory = db.directory(workspace).await?;
|
|
crate::store::account_people(&mut record.document, &directory)?;
|
|
let members = directory.iter().map(|u| u.id).collect();
|
|
let now = Utc::now();
|
|
let mut expanded = generation::expand(
|
|
&record.document,
|
|
workspace,
|
|
&draft.variables,
|
|
&draft.rules,
|
|
draft.route_position,
|
|
&record.document.working_interval,
|
|
&members,
|
|
now,
|
|
);
|
|
let used: std::collections::BTreeSet<_> = expanded
|
|
.document
|
|
.schedules
|
|
.iter()
|
|
.flat_map(|s| match &s.kind {
|
|
ScheduleKind::Rotation { participants, .. }
|
|
| ScheduleKind::Priority { participants, .. } => participants.clone(),
|
|
})
|
|
.chain(expanded.document.escalations.iter().flat_map(|e| {
|
|
e.steps.iter().filter_map(|s| match s.recipient {
|
|
Recipient::User { id } => Some(id),
|
|
_ => None,
|
|
})
|
|
}))
|
|
.collect();
|
|
for user in &directory {
|
|
if used.contains(&user.id) && expanded.document.person(user.id).is_none() {
|
|
expanded.document.people.push(user.person.clone());
|
|
}
|
|
}
|
|
if expanded.issues.is_empty() {
|
|
if let Err(e) = expanded.document.validate(&members) {
|
|
expanded.issues.push(Issue {
|
|
path: "People".into(),
|
|
message: e.to_string(),
|
|
..Issue::default()
|
|
});
|
|
}
|
|
if let Err(e) = providers::validate_capabilities(&expanded.document, now) {
|
|
expanded.issues.push(Issue {
|
|
path: "Provider".into(),
|
|
message: e.to_string(),
|
|
..Issue::default()
|
|
});
|
|
}
|
|
// Both shared and manually configured calendars must remain valid under DST.
|
|
if let Err(e) = engine::timeline(
|
|
&expanded.document,
|
|
now,
|
|
now + Duration::days(i64::from(expanded.document.horizon_days)),
|
|
) {
|
|
expanded.issues.push(Issue {
|
|
path: "Calendar".into(),
|
|
message: e.to_string(),
|
|
..Issue::default()
|
|
});
|
|
}
|
|
}
|
|
|
|
let issues = expanded.issues;
|
|
let cancelled = record
|
|
.document
|
|
.requests
|
|
.iter()
|
|
.filter(|r| {
|
|
r.status == RequestStatus::Pending
|
|
&& expanded
|
|
.document
|
|
.requests
|
|
.iter()
|
|
.any(|n| n.id == r.id && n.status == RequestStatus::Cancelled)
|
|
})
|
|
.count();
|
|
let result=issues.is_empty().then(||{let d=&expanded.document;json!({"timezone":d.timezone,"people":d.people,"schedules":d.schedules,"escalations":d.escalations,"routes":d.routes,"managed":d.generation.managed})});
|
|
let review = generation::stable_id(&json!([
|
|
workspace,
|
|
draft.revision,
|
|
draft.variables,
|
|
draft.rules,
|
|
draft.route_position
|
|
]))
|
|
.to_string();
|
|
let publication_changes = crate::store::publication_changes(
|
|
&expanded.document,
|
|
db.published_document(workspace).await?.as_ref(),
|
|
)?;
|
|
let report = json!({"shared_revision":draft.shared_revision,"publication_changes":publication_changes,"review_id":review,"changes":expanded.changes,"cancelled_requests":cancelled,"result":result,"trace":expanded.trace,"issues":issues});
|
|
record.document = expanded.document;
|
|
Ok(Prepared {
|
|
report,
|
|
record,
|
|
issues,
|
|
review,
|
|
})
|
|
}
|
|
pub async fn preview(db: &Store, actor: Uuid, workspace: Uuid, draft: Draft) -> Result<Value> {
|
|
Ok(prepare(db, actor, workspace, &draft).await?.report)
|
|
}
|
|
pub async fn apply(db: &Store, actor: Uuid, workspace: Uuid, draft: Draft) -> Result<Value> {
|
|
let mut prepared = prepare(db, actor, workspace, &draft).await?;
|
|
ensure(
|
|
prepared.issues.is_empty(),
|
|
prepared
|
|
.issues
|
|
.iter()
|
|
.take(8)
|
|
.map(|i| format!("{}: {}", i.path, i.message))
|
|
.collect::<Vec<_>>()
|
|
.join("; "),
|
|
)?;
|
|
ensure(
|
|
draft.review_id.as_deref() == Some(prepared.review.as_str()),
|
|
crate::i18n::text("err_the_preview_is_stale_check_the_updated_impact_be_85cacd"),
|
|
)?;
|
|
db.commit(
|
|
&mut prepared.record,
|
|
actor,
|
|
Permission::EditSchedules,
|
|
"generation.applied",
|
|
prepared.report,
|
|
None,
|
|
)
|
|
.await?;
|
|
Ok(json!({"ok":true}))
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct Transfer {
|
|
pub revision: i64,
|
|
pub shared_revision: i64,
|
|
#[serde(default)]
|
|
pub text: String,
|
|
#[serde(default)]
|
|
pub review_id: Option<String>,
|
|
}
|
|
async fn transfer_base(
|
|
db: &Store,
|
|
actor: Uuid,
|
|
workspace: Uuid,
|
|
request: &Transfer,
|
|
) -> Result<(WorkspaceRecord, Draft)> {
|
|
let record = db
|
|
.prepare(
|
|
workspace,
|
|
request.revision,
|
|
actor,
|
|
Permission::EditSchedules,
|
|
)
|
|
.await?;
|
|
let shared = db.shared_draft(workspace).await?;
|
|
ensure(
|
|
shared["revision"].as_i64() == Some(request.shared_revision),
|
|
crate::i18n::text("err_shared_draft_stale"),
|
|
)?;
|
|
let mut draft: Draft = if shared["payload"].is_null() {
|
|
Draft {
|
|
revision: record.revision,
|
|
shared_revision: Some(request.shared_revision),
|
|
variables: record.document.generation.variables.clone(),
|
|
rules: record.document.generation.rules.clone(),
|
|
route_position: record.document.generation.route_position,
|
|
review_id: None,
|
|
}
|
|
} else {
|
|
serde_json::from_value(shared["payload"].clone())?
|
|
};
|
|
draft.revision = record.revision;
|
|
draft.shared_revision = Some(request.shared_revision);
|
|
Ok((record, draft))
|
|
}
|
|
pub async fn export_config(
|
|
db: &Store,
|
|
actor: Uuid,
|
|
workspace: Uuid,
|
|
request: Transfer,
|
|
) -> Result<Value> {
|
|
let (record, draft) = transfer_base(db, actor, workspace, &request).await?;
|
|
Ok(
|
|
json!({"text":crate::portable::format_config(&crate::portable::export(&record.document,&draft)?)}),
|
|
)
|
|
}
|
|
pub async fn import_config(
|
|
db: &Store,
|
|
actor: Uuid,
|
|
workspace: Uuid,
|
|
request: Transfer,
|
|
apply: bool,
|
|
) -> Result<Value> {
|
|
let (record, base) = transfer_base(db, actor, workspace, &request).await?;
|
|
let (draft, inputs) =
|
|
crate::portable::import(workspace, &record.document, &base, &request.text)?;
|
|
let prepared = prepare(db, actor, workspace, &draft).await?;
|
|
let review = generation::stable_id(&json!([
|
|
prepared.review,
|
|
request.shared_revision,
|
|
request.text
|
|
]))
|
|
.to_string();
|
|
if apply {
|
|
ensure(
|
|
prepared.issues.is_empty(),
|
|
crate::i18n::text("transfer_fix_errors"),
|
|
)?;
|
|
ensure(
|
|
request.review_id.as_deref() == Some(review.as_str()),
|
|
crate::i18n::text("err_the_preview_is_stale_check_the_updated_impact_be_85cacd"),
|
|
)?;
|
|
return db
|
|
.save_shared_draft(
|
|
workspace,
|
|
actor,
|
|
request.shared_revision,
|
|
serde_json::to_value(draft)?,
|
|
)
|
|
.await;
|
|
}
|
|
let contacts: Vec<_> = prepared
|
|
.record
|
|
.document
|
|
.people
|
|
.iter()
|
|
.filter(|p| record.document.person(p.id).is_none() && p.user_id.is_none())
|
|
.collect();
|
|
Ok(json!({"review_id":review,"inputs":inputs,"contacts":contacts,"preview":prepared.report}))
|
|
}
|