1use super::*;
4
5pub(super) fn request_brief(r: &ChangeRequest) -> Value {
6 json!({
7 "number": r.number,
8 "title": r.title,
9 "status": r.status,
10 "reason": r.reason,
11 "priority": r.priority,
12 "requested_by": r.requested_by_name,
13 "assignee": r.assignee_name,
14 "due_date": r.due_date,
15 "item_count": r.item_count,
16 "created_at": r.created_at,
17 "updated_at": r.updated_at,
18 "closed_at": r.closed_at,
19 })
20}
21
22pub fn request_status_filter(raw: Option<&str>) -> AppResult<(Option<RequestStatus>, bool)> {
25 match raw.map(str::trim).unwrap_or("all") {
26 "" | "all" => Ok((None, false)),
27 "open" => Ok((None, true)),
28 s => RequestStatus::parse(s)
29 .map(|status| (Some(status), false))
30 .ok_or_else(|| {
31 bad(format!(
32 "Unknown status '{s}' ({}).",
33 REQUEST_STATUS_FILTERS.join(", ")
34 ))
35 }),
36 }
37}
38
39pub fn request_scope(
42 raw: Option<&str>,
43 me: uuid::Uuid,
44) -> AppResult<(Option<uuid::Uuid>, Option<uuid::Uuid>)> {
45 match raw.map(str::trim).unwrap_or("") {
46 "" => Ok((None, None)),
47 "mine" => Ok((Some(me), None)),
48 "assigned" => Ok((None, Some(me))),
49 s => Err(bad(format!("Unknown scope '{s}' (mine or assigned)."))),
50 }
51}
52
53pub(super) fn newest<T>(mut list: Vec<T>, max: usize) -> (Vec<T>, bool) {
55 let truncated = list.len() > max;
56 if truncated {
57 list.drain(..list.len() - max);
58 }
59 (list, truncated)
60}
61
62pub(super) async fn list_change_requests(
63 st: &AppState,
64 me: &Actor,
65 a: &Args<'_>,
66) -> AppResult<Value> {
67 let (status, open_only) = request_status_filter(a.opt_str("status")?.as_deref())?;
68 let (requested_by, assignee) = request_scope(a.opt_str("scope")?.as_deref(), me.id)?;
69 let item_id = match a.opt_str("item")? {
70 Some(number) => Some(items::get_by_number(&st.db, me, &number).await?.id),
71 None => None,
72 };
73 let limit = a.limit("limit", 20, 100)?;
74 let filter = requests::RequestFilter {
75 status,
76 open_only,
77 requested_by,
78 assignee,
79 item_id,
80 };
81 let (rows, total) = requests::list(&st.db, me, &filter, Page::new(Some(limit), None)).await?;
82 Ok(json!({
83 "total": total,
84 "truncated": total > rows.len() as i64,
85 "change_requests": rows.iter().map(request_brief).collect::<Vec<_>>(),
86 }))
87}
88
89async fn request_overview(st: &AppState, me: &Actor, request: &ChangeRequest) -> AppResult<Value> {
91 let id = request.id;
92 let ids = [id];
93 let (affected, comments, attachments, linked, history, can) = tokio::try_join!(
94 requests::items(&st.db, me, id),
95 requests::comments(&st.db, id),
96 requests::attachments(&st.db, id),
97 requests::linked_changes(&st.db, me, id),
98 audit::for_entities(&st.db, me, &ids, REQUEST_HISTORY + 1),
100 requests::abilities_of(&st.db, me, request),
101 )?;
102 let (comments, comments_cut) = newest(comments, MAX_COMMENTS);
103 let (history, history_cut) = capped(history, REQUEST_HISTORY as usize);
104 let mut brief = request_brief(request);
105 brief["description"] = json!(request.description);
106 if request.decided_at.is_some() {
107 brief["decision"] = json!({
108 "by": request.decided_by_name,
109 "at": request.decided_at,
110 "comment": request.decision_comment,
111 });
112 }
113 Ok(json!({
114 "change_request": brief,
115 "items": affected.iter().map(|i| json!({
116 "item": i.item_number,
117 "name": i.name,
118 "type": i.item_type,
119 "project": i.project_code,
120 "found_in_revision": i.revision_code,
121 "found_in_state": i.revision_state,
122 "latest_revision": i.latest_code,
123 "latest_state": i.latest_state,
124 "note": i.note,
125 })).collect::<Vec<_>>(),
126 "comments": comments.iter().map(|c| json!({
127 "author": c.author_name,
128 "at": c.created_at,
129 "body": c.body,
130 })).collect::<Vec<_>>(),
131 "comments_truncated": comments_cut,
132 "attachments": attachments.iter().map(|f| json!({
133 "filename": f.filename,
134 "content_type": f.content_type,
135 "size_bytes": f.size_bytes,
136 "uploaded_by": f.uploaded_by_name,
137 "uploaded_at": f.uploaded_at,
138 })).collect::<Vec<_>>(),
139 "changes": linked.0.iter().map(change_brief).collect::<Vec<_>>(),
140 "hidden_changes": linked.1,
141 "history": history.iter().map(|e| json!({
142 "at": e.at,
143 "user": e.username,
144 "action": e.action,
145 "summary": e.summary,
146 })).collect::<Vec<_>>(),
147 "history_truncated": history_cut,
148 "you_may": can,
149 "url": web_url(st, &format!("/requests/{}", request.id)),
150 }))
151}
152
153pub(super) async fn get_change_request(
154 st: &AppState,
155 me: &Actor,
156 a: &Args<'_>,
157) -> AppResult<Value> {
158 let request = requests::get_by_number(&st.db, me, &a.req_str("request")?).await?;
159 request_overview(st, me, &request).await
160}
161
162pub(super) async fn open_requests_of(
164 st: &AppState,
165 me: &Actor,
166 item_id: uuid::Uuid,
167) -> AppResult<(Vec<Value>, bool)> {
168 let (rows, cut) = capped(
169 requests::open_for_item(&st.db, me, item_id).await?,
170 MAX_RELATED,
171 );
172 Ok((rows.iter().map(request_brief).collect(), cut))
173}
174
175pub fn request_reason(raw: Option<&str>) -> AppResult<RequestReason> {
176 let raw = raw
177 .map(str::trim)
178 .filter(|s| !s.is_empty())
179 .unwrap_or("other");
180 RequestReason::parse(raw).ok_or_else(|| {
181 bad(format!(
182 "Unknown reason '{raw}' ({}).",
183 REQUEST_REASONS.join(", ")
184 ))
185 })
186}
187
188async fn affected_item(
190 st: &AppState,
191 me: &Actor,
192 reference: &str,
193) -> AppResult<requests::AffectedItem> {
194 let (number, code) = parse_item_ref(reference);
195 Ok(match code {
196 Some(code) => {
197 let rev = revision_at(st, me, &number, Some(&code)).await?;
198 requests::AffectedItem {
199 item_id: rev.item_id,
200 revision_id: Some(rev.id),
201 note: String::new(),
202 }
203 }
204 None => requests::AffectedItem {
205 item_id: items::get_by_number(&st.db, me, &number).await?.id,
206 revision_id: None,
207 note: String::new(),
208 },
209 })
210}
211
212const REQUEST_NEXT_STEPS: &str = "The change request is new. CAD editors of the affected project groups \
213 triage it in TeamCraft and accept, defer or reject it; an accepted request is implemented by a change \
214 order. The assistant cannot decide it.";
215
216pub(super) async fn create_change_request(
217 st: &AppState,
218 me: &Actor,
219 a: &Args<'_>,
220) -> AppResult<Value> {
221 let title = a.req_str("title")?;
222 let description = a.opt_str("description")?.unwrap_or_default();
223 let reason = request_reason(a.opt_str("reason")?.as_deref())?;
224 let priority = a.opt_str("priority")?.unwrap_or_else(|| "normal".into());
225 let priority = Priority::parse(&priority)
226 .ok_or_else(|| bad("priority must be low, normal, high or critical."))?;
227 let due_date = requests::parse_due_date(a.opt_str("due_date")?.as_deref())?;
228 let refs = a.list("items", requests::MAX_ITEMS as usize)?;
229 let mut affected = Vec::with_capacity(refs.len());
231 for r in &refs {
232 affected.push(affected_item(st, me, r).await?);
233 }
234 let request = requests::create(
235 &st.db,
236 me,
237 requests::RequestInput {
238 title,
239 description,
240 reason,
241 priority,
242 due_date,
243 },
244 affected,
245 )
246 .await?;
247 let mut value = request_overview(st, me, &request).await?;
248 value["next_steps"] = json!(REQUEST_NEXT_STEPS);
249 Ok(value)
250}
251
252pub(super) async fn comment_change_request(
253 st: &AppState,
254 me: &Actor,
255 a: &Args<'_>,
256) -> AppResult<Value> {
257 let request = requests::get_by_number(&st.db, me, &a.req_str("request")?).await?;
258 let body = a.req_str("body")?;
259 let comment = requests::comment(&st.db, me, request.id, &body).await?;
260 Ok(json!({
261 "change_request": request.number,
262 "status": request.status,
263 "comment": {
264 "author": comment.author_name,
265 "at": comment.created_at,
266 "body": comment.body,
267 },
268 "url": web_url(st, &format!("/requests/{}", request.id)),
269 }))
270}