hgame 0.26.4

CG production management structs, e.g. of assets, personnels, progress, etc.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
//! Fundamentally, the smallest element is [`Notice`], also called `review item`.
//! Daily, [`Notice`]s of various [`Supervision`] types are contributed by
//! artists/leaders/producers to form [`DailyNotice`].
//!
//! NOTE: Therefore, structs have "Daily" grouping whenever they each comprise
//! of various review item types, e.g. WIP, Feedback, Approval, Client.
//!
//! Series of [`DailyNotice`]s are grouped under [`NoticeSequence`] for each [`ProductionAsset`].
//!
//! Since `review item`s may not be viewed immediately within the day they are submitted,
//! a concept of "review verdict" is therefore employed to determine whether a `review item`
//! has been reviewed or not, -- its [`Reviewed`] variant --, within a given date range.
//! Similarly, [`Reviewed`] verdicts of different [`Supervision`] types are also grouped into
//! [`DailyReview`].
//!
//! The date range is provided via [`TrimOptions`] in [`Date<Utc>`], which
//! gets converted to [`NaiveTrimOptions`] in [`NaiveDate`] due to legacy design.
//!
//! Though the "review verdicts", [`DailyReview`] serves as intermediate data on which to
//! calculate higher level stats, namely the [`SupervisionStats`] -- in which variants of [`Reviewed`]
//! furthermore used to deduce whether an item has been reviewed or not, as this is what supervisors
//! care most about.
//!
//! Normally, supervisors not only need to know the reviewed states of multiple assets at once,
//! which asks firstly for the [`ProjReviewVerdict`], but they also need to review multiple productions
//! at once, which then asks for the [`PanProjectReviewStats`].

mod notice;
mod stats;

pub use notice::*;
pub use stats::*;

use super::*;
use chrono::prelude::{Local, NaiveDate, NaiveTime};
use mkutil::datetime::parse_naive_date;

#[derive(Debug, Clone, PartialEq, Eq, Hash)]

pub enum ReviewAction {
    /// Make the `AssetExcerpt` as active selection.
    Pilot(Project, AssetExcerpt),

    MarkReviewed {
        project: Project,
        asset: AssetExcerpt,
        typ: Supervision,
        is_next_to_last: bool,
        window_id: Option<String>,
    },

    MarkNotReviewed(Project, AssetExcerpt, Supervision),

    DebugNoticeSequence(Project, AssetExcerpt),
}

impl ReviewAction {
    fn pilot(project: &Project, asset: &AssetExcerpt) -> Self {
        Self::Pilot(project.clone(), asset.clone())
    }

    fn mark_reviewed(
        project: &Project,
        asset: &AssetExcerpt,
        typ: &Supervision,
        existing_unreviewed: &usize,
        window_id: &String,
    ) -> Self {
        let is_next_to_last = *existing_unreviewed == 1;
        Self::MarkReviewed {
            project: project.clone(),
            asset: asset.clone(),
            typ: typ.clone(),
            window_id: if is_next_to_last {
                Some(window_id.to_owned())
            } else {
                None
            },
            is_next_to_last,
        }
    }

    fn mark_not_reviewed(project: &Project, asset: &AssetExcerpt, typ: &Supervision) -> Self {
        Self::MarkNotReviewed(project.clone(), asset.clone(), typ.clone())
    }

    #[cfg(debug_assertions)]
    fn debug_notice_sequence(project: &Project, asset: &AssetExcerpt) -> Self {
        Self::DebugNoticeSequence(project.clone(), asset.clone())
    }
}

/// Asset-centric map to [`DailyReview`] for the processed time range, for example:
/// ```
/// {
///     Hanna: DailyReview { wip: Yes, feedback: No, approval: Yes, client: No },
///     Mia: DailyReview { wip: Yes, feedback: Yes, approval: No, client: Yes },
/// }
/// ```
pub type ProjReviewVerdict = HashMap<AssetExcerpt, DailyReview>;

// -------------------------------------------------------------------------------
#[derive(Debug, Clone, strum::AsRefStr, strum::EnumIter, PartialEq, Eq, Hash)]
/// DO NOT change `strum(serialize)` values as they are used as key literals
/// when inserting to database.
pub enum Supervision {
    #[strum(serialize = "wip_review")]
    Wip,
    #[strum(serialize = "feedback_review")]
    Feedback,
    #[strum(serialize = "approval_review")]
    Approval,
    #[strum(serialize = "client_feedback")]
    Client,
}

impl Supervision {
    /// Headers for table columns, from standpoint of Art Directors
    /// at Virtuos-SPARX*.
    pub fn ad_column_header(&self) -> &str {
        match self {
            Self::Wip => "WIP | Daily",
            Self::Feedback => "Feedback Complete",
            Self::Approval => "Submission for Review",
            Self::Client => "You Have Feedback",
        }
    }

    fn signaling_label(&self) -> &str {
        match self {
            Self::Wip => "🗠 WIP | Daily",
            Self::Feedback => "",
            Self::Approval => "🙏 For Approval",
            Self::Client => "🚔 Client Feedback",
        }
    }

    fn draft_tooltip(&self) -> &str {
        match self {
            Self::Wip => "Enable this review item after you submit\nWIP|Daily snapshots",
            Self::Feedback => "",
            Self::Approval => {
                "Enable this review item after you submit\nFor Approval snapshots, or UnitedTT movie"
            }
            Self::Client => "Enable this review item to add one count\nfor this asset in PanProject Review table stats",
        }
    }
}

// -------------------------------------------------------------------------------
#[async_trait]
pub trait ReviewStats: DynClone + fmt::Debug + Send + Sync {
    /// Gets state of the notice -- with specific review `typ` on the given `date`.
    async fn review_item(
        &mut self,
        project: &Project,
        asset: &ProductionAsset,
        typ: &Supervision,
        date: &DateTime<Local>,
    ) -> Result<Notice, DatabaseError>;

    /// Submits a [`Notice`]. Its inner review item type (`Notice::typ`) as well as its inner
    /// target (`Notice::enabled`) will be used. The `submit_time` will be formatted into both
    /// date and time separatedly.
    /// The notice may already exists (as we allow users to toggle a notice's submit state).
    /// The "viewed by" state(s) of a [`Notice`] therfore should also be reset (to `false`)
    /// everytime it gets re-submitted.
    async fn submit_notice(
        &mut self,
        project: &Project,
        asset: &ProductionAsset,
        notice: Notice,
        submit_time: &DateTime<Local>,
    ) -> Result<(), ModificationError>;

    /// Alters the "viewed by" state of the given [`Notice`]. Its inner review item type
    /// (`Notice::typ`) will be used.
    async fn mark_viewed(
        &mut self,
        project: &Project,
        asset: &ProductionAsset,
        typ: &Supervision,
        submit_time: &DateTime<Local>,
        reviewer: &ProductionRole,
        target: bool,
    ) -> Result<(), ModificationError>;

    async fn asset_notice_sequence(
        &mut self,
        project: &Project,
        asset: &ProductionAsset,
        trim_options: &TrimOptions,
    ) -> Result<NoticeSequence, DatabaseError>;

    /// "Review verdict" for a single asset within the given date range.
    async fn asset_review_verdict(
        &mut self,
        project: &Project,
        asset: &ProductionAsset,
        trim_options: &TrimOptions,
        reviewer: &ProductionRole,
    ) -> Result<DailyReview, DatabaseError>;

    /// Given the date range, gets the stats -- count of all the assets that require supervision,
    /// i.e. all assets that yield verdict of `Reviewed::No`
    async fn project_review_stats(
        &mut self,
        project: &Project,
        trim_options: &TrimOptions,
        reviewer: &ProductionRole,
    ) -> Result<ProjReviewStats, DatabaseError>;

    /// Given a list of projects, get review stats for each of them.
    async fn panproject_review_stats(
        &mut self,
        pp_members: &[Project],
        trim_options: &TrimOptions,
        reviewer: &ProductionRole,
    ) -> Result<PanProjectReviewStats, DatabaseError>;

    fn clear_cache(&mut self) {}
}

dyn_clone::clone_trait_object!(ReviewStats);

// -------------------------------------------------------------------------------
/// The "receiving/processing notices" part that should be examined together with
/// a `NoticeSequence::sequence_date` -- after each of its review type has been trimmed
/// with a date range.
/// ATTENTION: Since given a "review sequence" of a date range (in chronological order), we'll want to see to make a "verdict"
/// on whether a review item has been reviewed (the [`Reviewed`] variant) or not, it's more
/// convenient to reverse the review sequence.
// An original "review sequence" has the form of, for example:
// [
//     (2022-03-04, DailyReview {
//         wip: NoSubmission,
//         feedback: No,
//         approval: No,
//         client: Yes
//     }),
//     (2022-03-08, DailyReview {
//         wip: NoSubmission,
//         feedback: No,
//         approval: Yes,
//         client: No
//     })
// ]
// (This normally will be reversed while making a verdict, e.g. 2022-03-08 -> 2022-03-04)
type ReviewSequence = Vec<(NaiveDate, DailyReview)>;

#[derive(Debug, Clone)]
/// Finding a "review verdict" means deciding the current [`Reviewed`] state of a review item,
/// given a "review sequence" -- per item type -- such as
/// [Reviewed::NoSubmission, Reviewed::Yes, Reviewed::No, Reviewed::NoSubmission].
/// The verdicts are dependent of the moment of querying.
pub struct ReviewVerdictBuilder(ReviewSequence);

impl ReviewVerdictBuilder {
    /// Converts a [`NoticeSequence`] into an intermediate mode of [`ReviewSequence`],
    /// before being made into [`DailyReview`].
    pub fn new(seq: &NoticeSequence, reviewer_role: &ProductionRole) -> Self {
        let mut sequence = vec![];
        for (date, v) in seq.sequence_date.iter() {
            // skips all dates where `DailyNotice` has been taken (by being previously empty
            // as a result of `NoticeSequenceReadBuilder`)
            if let Some(notice) = v {
                sequence.push((date.clone(), notice.is_reviewed_by(reviewer_role)));
            };
        }
        Self(
            // reverses the order,
            sequence.into_iter().rev().collect(),
        )
    }

    /// Recursively finds "[`Reviewed`] verdict" for each review type.
    pub fn build(self) -> DailyReview {
        DailyReview {
            wip: Reviewed::verdict(self.0.iter().map(|r| &r.1.wip)),
            feedback: Reviewed::verdict(self.0.iter().map(|r| &r.1.feedback)),
            approval: Reviewed::verdict(self.0.iter().map(|r| &r.1.approval)),
            client: Reviewed::verdict(self.0.iter().map(|r| &r.1.client)),
        }
    }
}

// -------------------------------------------------------------------------------
#[derive(Debug, Clone, Serialize, Deserialize)]
/// The "sending notices" part, i.e. not taking into account whether the
/// notices have been viewed/reviewed by the recipient(s).
/// When you have a [`NoticeSequence`], it can be turned into "review verdict",
/// -- a [`DailyReview`] -- by using [`ReviewVerdictBuilder`].
/// LEGACY DESIGN: DO NOT change field names or `serde(rename)` values.
pub struct NoticeSequence {
    #[serde(rename = "daily_review")]
    /// Original `daily_review` field.  Series of [`DailyNotice`]s in a table
    /// by date in `String` and of `%Y-%m-%d` format, e.g.
    /// {"2022-10-24": DailyNotice(...), "2022-10-25": DailyNotice(...)}
    /// After deserialization the `Self::sequence_date` should be used instead, as
    /// this can be made into `Option::None`.
    pub sequence_str: Option<HashMap<String, DailyNotice>>,
    #[serde(skip)]
    /// Data converted from `Self::sequence_str` with [`NaiveDate`] instead of `String`, and
    /// should also be sorted chronologically, i.e. by date.
    /// SAFETY: this should NEVER be used to convert back to `Self::sequence_str` for serialization,
    /// since normally it will be mutated -- and severed -- with [`TrimOptions`].
    pub sequence_date: Vec<(NaiveDate, Option<DailyNotice>)>,
}

impl NoticeSequence {
    /// Removes the `None` `DailyNotice`s.
    pub fn filter_range(self) -> Vec<(NaiveDate, DailyNotice)> {
        self.sequence_date
            .into_iter()
            .filter_map(|(date, notice)| match notice {
                Some(notice) => Some((date, notice)),
                None => None,
            })
            .collect()
    }

    pub fn debug_range(self) -> String {
        let mut seq = String::new();
        self.filter_range().iter().for_each(|(date, notice)| {
            seq.push_str(&format!("\n---- {} ----", date));
            seq.push_str(&format!("\n{} {:?}", Supervision::Wip.as_ref(), notice.wip));
            seq.push_str(&format!(
                "\n{} {:?}",
                Supervision::Feedback.as_ref(),
                notice.feedback
            ));
            seq.push_str(&format!(
                "\n{} {:?}",
                Supervision::Approval.as_ref(),
                notice.approval
            ));
            seq.push_str(&format!(
                "\n{} {:?}",
                Supervision::Client.as_ref(),
                notice.client
            ));
        });
        seq
    }

    /// Given the [`TrimOptions`], depending on each review type trim range, if any,
    /// mutates each corresponding field of the [`DailyNotice`]s.
    /// PERFORMANCE NOTE: use [`Self::trim_naive`] instead for performance gain and to
    /// avoid conversion every time.
    fn trim(&mut self, trim_options: &TrimOptions) {
        let trim_options: NaiveTrimOptions = trim_options.into();
        for (k, v) in self.sequence_date.iter_mut() {
            if let Some(notice) = v {
                notice.trim(k, &trim_options);
                if notice.is_empty() {
                    // not retaining `DailyNotice`s whose all fields `is_none`
                    v.take();
                };
            };
        }
    }

    fn trim_naive(&mut self, trim_options: &NaiveTrimOptions) {
        for (k, v) in self.sequence_date.iter_mut() {
            if let Some(notice) = v {
                notice.trim(k, &trim_options);
                if notice.is_empty() {
                    // not retaining `DailyNotice`s whose all fields `is_none`
                    v.take();
                };
            };
        }
    }
}

// -------------------------------------------------------------------------------
#[derive(Debug)]
/// Processes a [`NoticeSequence`] by:
///   - converting submit time from `String` to `NaiveTime` with the given format str,
///   - converting review date from `String` to `NaiveDate` with the given format str,
///   - trimming -- removing all review items that do not belong to the given date range
/// of the [`TrimOptions`].
pub struct NoticeSequenceReadBuilder(NoticeSequence);

impl NoticeSequenceReadBuilder {
    pub fn new(seq: NoticeSequence) -> Self {
        Self(seq)
    }

    /// Removes all [`DailyNotice`]s which do not belong to the given [`DateRange`].
    /// Remember to invoke this after [`Self::parse_review_date`] to work with [`NaiveDate`]s
    /// instead of `String`s.
    fn trim(mut self, trim_options: &TrimOptions) -> Self {
        self.0.trim(trim_options);
        self
    }

    fn trim_naive(mut self, trim_options: &NaiveTrimOptions) -> Self {
        self.0.trim_naive(trim_options);
        self
    }

    /// Converts `submit_time` from `String` to `NaiveTime`.
    fn parse_submit_time(mut self, fmt: &str) -> Self {
        if let Some(sequence) = self.0.sequence_str.as_mut() {
            sequence.values_mut().for_each(|d| d.parse_submit_time(fmt));
        };
        self
    }

    /// Maps date as `String` to [`NaiveDate`] for `Self::sequence_date`,
    /// leaving `Self::sequence_str` to `Option::None`.
    /// For `fmt` arg, `hconf::ClientCfgCel::settings::notice_submit_day_format`
    /// can be used.
    fn parse_review_date(mut self, fmt: &str) -> Self {
        if let Some(sequence) = self.0.sequence_str.take() {
            let mut seq_date = vec![];
            for (k, v) in sequence.into_iter() {
                match parse_naive_date(&k, fmt) {
                    Ok(date) => {
                        seq_date.push((date, Some(v)));
                    }
                    Err(e) => {
                        // skips all parse fails
                        error!("Skipping DailyNotice of {:?} due to: {}", k, e);
                    }
                }
            }
            // sorts by date, from the later to the sooner, e.g. 2022-03-04 -> 2022-03-08
            seq_date.sort_by_key(|s| s.0.to_owned());
            // remember to assign back to `self.0`
            self.0.sequence_date = seq_date;
        };
        self
    }

    /// Remember to use [`Self::finish_reuse`] if you're iterating over a lot of documents.
    pub fn finish(
        self,
        time_format: &str,
        date_format: &str,
        trim_options: &TrimOptions,
    ) -> NoticeSequence {
        self.parse_submit_time(time_format)
            .parse_review_date(date_format)
            .trim(trim_options)
            .0
    }

    /// Finishes with already converted [`NaiveTrimOptions`], but omits the step
    /// of [`Self::parse_submit_time`] for efficiency, since many [`DailyNotice`]s
    /// could be trimmed.
    pub fn finish_reuse(
        self,
        date_format: &str,
        trim_options: &NaiveTrimOptions,
    ) -> NoticeSequence {
        self.parse_review_date(date_format)
            .trim_naive(trim_options)
            .0
    }
}

// -------------------------------------------------------------------------------

pub fn submit_date_str(date: &DateTime<Local>) -> AnyResult<String> {
    Ok(format!("{}", date.format(date_fmt_cfg()?)))
}

fn submit_time_str(time: &DateTime<Local>) -> AnyResult<String> {
    Ok(format!("{}", time.format(time_fmt_cfg()?)))
}

/// Format time string from config.
pub fn time_fmt_cfg() -> AnyResult<&'static String> {
    let settings = ClientCfgCel::settings().as_ref()?;
    Ok(settings.notice_submit_time_format())
}

/// Format date string from config.
pub fn date_fmt_cfg() -> AnyResult<&'static String> {
    let settings = ClientCfgCel::settings().as_ref()?;
    Ok(settings.notice_submit_day_format())
}