evergreen/common/
org.rs

1use crate as eg;
2use chrono::prelude::Datelike;
3use chrono::Duration;
4use eg::date;
5use eg::Editor;
6use eg::EgResult;
7use eg::EgValue;
8
9/// Apply a variety of DB transforms to an org unit and return
10/// the calculated org unit IDs.
11fn org_relations_query(
12    editor: &mut Editor,
13    org_id: i64,
14    transform: &str,
15    depth: Option<i64>,
16) -> EgResult<Vec<i64>> {
17    let mut query = eg::hash! {
18        "select": {
19            "aou": [{
20                "transform": transform,
21                "column": "id",
22                "result_field": "id",
23                "params": []
24            }]
25        },
26        "from": "aou",
27        "where": {"id": org_id}
28    };
29
30    if let Some(d) = depth {
31        query["select"][0]["params"] = EgValue::from(vec![d]);
32    }
33
34    let list = editor.json_query(query)?;
35
36    let mut ids = Vec::new();
37    for h in list {
38        ids.push(h.id()?);
39    }
40    Ok(ids)
41}
42
43pub fn by_shortname(editor: &mut Editor, sn: &str) -> EgResult<EgValue> {
44    if let Some(o) = editor.search("aou", eg::hash! {"shortname": sn})?.pop() {
45        Ok(o)
46    } else {
47        Err(editor.die_event())
48    }
49}
50
51pub fn ancestors(editor: &mut Editor, org_id: i64) -> EgResult<Vec<i64>> {
52    org_relations_query(editor, org_id, "actor.org_unit_ancestors", None)
53}
54
55pub fn descendants(editor: &mut Editor, org_id: i64) -> EgResult<Vec<i64>> {
56    org_relations_query(editor, org_id, "actor.org_unit_descendants", None)
57}
58
59pub fn full_path(editor: &mut Editor, org_id: i64, depth: Option<i64>) -> EgResult<Vec<i64>> {
60    org_relations_query(editor, org_id, "actor.org_unit_full_path", depth)
61}
62
63/// Returns the root org unit object, which is the first parent-less org unit
64/// found.
65///
66/// # Panics
67///
68/// Panics if the system has no root org unit.
69pub fn root_org_unit(editor: &mut Editor) -> EgResult<EgValue> {
70    Ok(editor
71        .search_with_ops(
72            "aou",
73            eg::hash! {"parent_ou": EgValue::Null},
74            eg::hash! {"limit": 1},
75        )?
76        .pop()
77        .expect("we require a root org unit")
78    )
79}
80
81/// Conveys the open state of an org unit on a specific day.
82#[derive(Clone, PartialEq)]
83pub enum OrgOpenState {
84    /// Open on the requested date.
85    Open,
86    /// Org unit is never open.
87    Never,
88    /// Org unit is closed on the requested day and will be open
89    /// again on the day representd by this date.
90    OpensOnDate(date::EgDate),
91}
92
93/// Returns an OrgOpenState descibing the open state of the org unit
94/// on the provided day in the timezone of the provided date.
95///
96/// If the result is OrgOpenState::OpensOnDate(date), the date value
97/// will be a fully-qualified DateTime with fixed timezone (so the
98/// original time zone can be retained).  However, only the date portion
99/// of the datetime is meaningful.  To get the final unadorned Date,
100/// in the timezone of the returned DateTime, without time or timzone:
101/// date.date_naive()
102pub fn next_open_date(
103    editor: &mut Editor,
104    org_id: i64,
105    date: &date::EgDate,
106) -> EgResult<OrgOpenState> {
107    let start_date = *date; // clone
108    let mut date = *date; // clone
109
110    let mut closed_days: Vec<i64> = Vec::new();
111    if let Some(h) = editor.retrieve("aouhoo", org_id)? {
112        for day in 0..7 {
113            let open = h[&format!("dow_{day}_open")].as_str().unwrap();
114            let close = h[&format!("dow_{day}_close")].as_str().unwrap();
115            if open == "00:00:00" && close == open {
116                closed_days.push(day);
117            }
118        }
119
120        // Always closed.
121        if closed_days.len() == 7 {
122            return Ok(OrgOpenState::Never);
123        }
124    }
125
126    let mut counter = 0;
127    while counter < 366 {
128        // inspect at most 1 year of data
129        counter += 1;
130
131        // Zero-based day of week
132        let weekday = date.date_naive().weekday().num_days_from_sunday();
133
134        if closed_days.contains(&(weekday as i64)) {
135            // Closed for the current day based on hours of operation.
136            // Jump ahead one day and start over.
137            date += Duration::try_days(1).expect("In Bounds");
138            continue;
139        }
140
141        // Open this day based on hours of operation.
142        // See if any overlapping closings are configured instead.
143
144        let timestamp = date::to_iso(&date);
145        let query = eg::hash! {
146            "org_unit": org_id,
147            "close_start": {"<=": EgValue::from(timestamp.clone())},
148            "close_end": {">=": EgValue::from(timestamp)},
149        };
150
151        let org_closed = editor.search("aoucd", query)?;
152
153        if org_closed.is_empty() {
154            // No overlapping closings.  We've found our open day.
155            if start_date == date {
156                // No changes were made.  We're open on the requested day.
157                return Ok(OrgOpenState::Open);
158            } else {
159                // Advancements were made to the date in progress to
160                // find an open day.
161                return Ok(OrgOpenState::OpensOnDate(date));
162            }
163        }
164
165        // Find the end of the closed date range and jump ahead to that.
166        let mut range_end = org_closed[0]["close_end"].as_str().unwrap();
167        for day in org_closed.iter() {
168            let end = day["close_end"].as_str().unwrap();
169            if end > range_end {
170                range_end = end;
171            }
172        }
173
174        date = date::parse_datetime(range_end)?;
175        date += Duration::try_days(1).expect("In Bounds");
176    }
177
178    // If we get here it means we never found an open day.
179    Ok(OrgOpenState::Never)
180}
181
182/// Returns the proximity from from_org to to_org.
183pub fn proximity(editor: &mut Editor, from_org: i64, to_org: i64) -> EgResult<Option<i64>> {
184    let query = eg::hash! {
185        "select": {"aoup": ["prox"]},
186        "from": "aoup",
187        "where": {
188            "from_org": from_org,
189            "to_org": to_org
190        }
191    };
192
193    if let Some(prox) = editor.json_query(query)?.pop() {
194        Ok(prox["prox"].as_int())
195    } else {
196        Ok(None)
197    }
198}