commit
a47c4a0398
96 files changed
+25721
No files matched your search
@@ -0,0 +1,163 @@
|
||||
//! 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;
|
||||
use serde_json::{Value, json};
|
||||
use uuid::Uuid;
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Draft {
|
||||
pub revision: 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?;
|
||||
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(Person {
|
||||
id: user.id,
|
||||
user_id: Some(user.id),
|
||||
display_name: user.display_name.clone(),
|
||||
email: None,
|
||||
country: Some(user.country.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 report = json!({"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}))
|
||||
}
|
||||
Reference in new issue
Block a user