Просмотр исходного кода

Merge branch 'master' of github.com:darkrenaissance/darkfi

lunar-mining 4 лет назад
Родитель
Сommit
720083af3f

+ 23 - 0
Cargo.lock

@@ -736,6 +736,17 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
 
+[[package]]
+name = "colored"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd"
+dependencies = [
+ "atty",
+ "lazy_static",
+ "winapi",
+]
+
 [[package]]
 name = "concurrent-queue"
 version = "1.2.4"
@@ -3973,12 +3984,15 @@ dependencies = [
  "async-std",
  "chrono",
  "clap 3.2.16",
+ "colored",
  "darkfi",
+ "fxhash",
  "log",
  "prettytable-rs",
  "serde",
  "serde_json",
  "simplelog",
+ "term_grid",
  "url",
 ]
 
@@ -4037,6 +4051,15 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "term_grid"
+version = "0.2.0"
+source = "git+https://github.com/Dastan-glitch/rust-term-grid.git#51c28e47aad0fb9f67de558e9b80c525cd01a4ae"
+dependencies = [
+ "colored",
+ "unicode-width",
+]
+
 [[package]]
 name = "termcolor"
 version = "1.1.3"

+ 3 - 0
bin/tau/tau-cli/Cargo.toml

@@ -14,10 +14,13 @@ categories = ["command-line-utilities"]
 async-std = {version = "1.12.0", features = ["attributes"]}
 chrono = "0.4.19"
 clap = {version = "3.2.16", features = ["derive"]}
+colored = "2.0.0"
 darkfi = { path = "../../../", features = ["rpc"]}
+fxhash = "0.2.1"
 log = "0.4.17"
 prettytable-rs = "0.8.0"
 serde = {version = "1.0.142", features = ["derive"]}
 serde_json = "1.0.83"
 simplelog = "0.12.0"
+term_grid = { git = "https://github.com/Dastan-glitch/rust-term-grid.git" }
 url = "2.2.2"

+ 130 - 0
bin/tau/tau-cli/src/drawdown.rs

@@ -0,0 +1,130 @@
+use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, Utc};
+use colored::Colorize;
+use fxhash::FxHashMap;
+use term_grid::{Cell, Direction, Filling, Grid, GridOptions};
+
+use darkfi::{Error, Result};
+
+use crate::primitives::{TaskEvent, TaskInfo};
+
+pub fn drawdown(date: String, tasks: Vec<TaskInfo>, owner: String) -> Result<()> {
+    let mut ret = FxHashMap::default();
+    let all_owners = owners(tasks.clone());
+
+    for owner in all_owners {
+        let stopped_tasks = tasks
+            .clone()
+            .into_iter()
+            .filter(|t| t.state == "stop" && t.owner == owner)
+            .collect::<Vec<TaskInfo>>();
+        ret.insert(owner, stopped_tasks);
+    }
+
+    let mut naivedate = to_naivedate(date.clone())?;
+
+    println!("log drawdown for {} in {}", owner, naivedate.format("%b %Y").to_string());
+
+    let fdow = if naivedate.month() == 2 && !is_leap_year(naivedate.year()) {
+        ["   ", "1 ", "8 ", "15", "22", " "]
+    } else {
+        ["   ", "1 ", "8 ", "15", "22", "29"]
+    };
+
+    // Print first day of each week horizontally.
+    let mut dow_grid =
+        Grid::new(GridOptions { direction: Direction::LeftToRight, filling: Filling::Spaces(1) });
+    if ret.contains_key(&owner) {
+        for i in fdow {
+            let cell = Cell::from(i);
+            dow_grid.add(cell)
+        }
+        let grid_display = dow_grid.fit_into_rows(1);
+        print!("{}", grid_display);
+    }
+
+    let mut grid =
+        Grid::new(GridOptions { direction: Direction::TopToBottom, filling: Filling::Spaces(1) });
+
+    let days_in_month = get_days_from_month(date) as u32;
+
+    if ret.contains_key(&owner) {
+        for _ in 0..7 {
+            let dow = naivedate.weekday().to_string();
+            let wcell = Cell::from(dow);
+            grid.add(wcell);
+            naivedate = naivedate + Duration::days(1);
+        }
+        for day in 1..=days_in_month {
+            let owner_stopped_tasks = ret.get(&owner).unwrap().to_owned();
+            let date_tasks: Vec<TaskInfo> = owner_stopped_tasks
+                .into_iter()
+                .filter(|t| {
+                    // last event is always state stop
+                    let event_date = NaiveDateTime::from_timestamp(
+                        t.events.last().unwrap_or(&TaskEvent::default()).timestamp.0,
+                        0,
+                    );
+                    event_date.day() == day
+                })
+                .collect();
+
+            let red_scale = if date_tasks.is_empty() {
+                50
+            } else {
+                ((date_tasks.len() * 25) + 90).clamp(90, 255) as u8
+            };
+
+            let cell = Cell::from("▀▀".truecolor(red_scale, 40, 50));
+            grid.add(cell)
+        }
+    }
+
+    let grid_display = grid.fit_into_rows(7);
+    println!("{}", grid_display);
+
+    Ok(())
+}
+
+fn is_leap_year(year: i32) -> bool {
+    return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
+}
+
+pub fn to_naivedate(date: String) -> Result<NaiveDate> {
+    if date.len() != 4 || date.parse::<u32>().is_err() {
+        return Err(Error::MalformedPacket)
+    }
+    let (month, year) = (date[..2].parse::<u32>().unwrap(), date[2..].parse::<i32>().unwrap());
+    let year = year + (Utc::today().year() / 100) * 100;
+
+    Ok(NaiveDate::from_ymd(year, month, 1))
+}
+
+fn get_days_from_month(date: String) -> i64 {
+    let (month, year) = (date[..2].parse::<u32>().unwrap(), date[2..].parse::<i32>().unwrap());
+    let year = year + (Utc::today().year() / 100) * 100;
+
+    NaiveDate::from_ymd(
+        match month {
+            12 => year + 1,
+            _ => year,
+        },
+        match month {
+            12 => 1,
+            _ => month + 1,
+        },
+        1,
+    )
+    .signed_duration_since(NaiveDate::from_ymd(year, month, 1))
+    .num_days()
+}
+
+fn owners(tasks: Vec<TaskInfo>) -> Vec<String> {
+    let mut owners = vec![];
+    for task in tasks {
+        if !owners.contains(&task.owner) {
+            owners.push(task.owner)
+        }
+    }
+
+    owners
+}

+ 4 - 13
bin/tau/tau-cli/src/filter.rs

@@ -1,22 +1,13 @@
 use chrono::{Datelike, NaiveDateTime, Utc};
 use serde_json::Value;
 
-use crate::{
-    primitives::{State, TaskInfo},
-    TaskEvent,
-};
-
-/// Helper function to check task's state
-fn check_task_state(task: &TaskInfo, state: State) -> bool {
-    let last_state = task.events.last().unwrap_or(&TaskEvent::default()).action.clone();
-    state.to_string() == last_state
-}
+use crate::primitives::{State, TaskInfo};
 
 pub fn apply_filter(tasks: &mut Vec<TaskInfo>, filter: &str) {
     match filter {
-        "open" => tasks.retain(|task| check_task_state(task, State::Open)),
-        "start" => tasks.retain(|task| check_task_state(task, State::Start)),
-        "pause" => tasks.retain(|task| check_task_state(task, State::Pause)),
+        "open" => tasks.retain(|task| task.state == State::Open.to_string()),
+        "start" => tasks.retain(|task| task.state == State::Start.to_string()),
+        "pause" => tasks.retain(|task| task.state == State::Pause.to_string()),
 
         _ if filter.len() == 4 && filter.parse::<u32>().is_ok() => {
             let (month, year) =

+ 15 - 1
bin/tau/tau-cli/src/main.rs

@@ -11,12 +11,14 @@ use darkfi::{
     Result,
 };
 
+mod drawdown;
 mod filter;
 mod primitives;
 mod rpc;
 mod util;
 mod view;
 
+use drawdown::{drawdown, to_naivedate};
 use primitives::{task_from_cli, State, TaskEvent};
 use util::{desc_in_editor, due_as_timestamp};
 use view::{comments_as_string, print_task_info, print_task_list};
@@ -84,6 +86,9 @@ enum TauSubcommand {
 
     /// Export tasks to a specified directory.
     Export { path: Option<String> },
+
+    /// Drawdown.
+    Log { month: String, owner: String },
 }
 
 pub struct Tau {
@@ -135,7 +140,7 @@ async fn main() -> Result<()> {
                 }
                 None => {
                     let task = tau.get_task_by_id(task_id).await?;
-                    let state = &task.events.last().unwrap_or(&TaskEvent::default()).action.clone();
+                    let state = State::from_str(&task.state)?;
                     println!("Task {}: {}", task_id, state);
                     Ok(())
                 }
@@ -174,6 +179,7 @@ async fn main() -> Result<()> {
 
                 Ok(())
             }
+
             TauSubcommand::Import { path } => {
                 let path = path.unwrap_or(DEFAULT_PATH.into());
                 let res = tau.import_from(path.clone()).await?;
@@ -186,6 +192,14 @@ async fn main() -> Result<()> {
 
                 Ok(())
             }
+
+            TauSubcommand::Log { month, owner } => {
+                let ts = to_naivedate(month.clone())?.and_hms(12, 0, 0).timestamp();
+                let tasks = tau.get_stop_tasks(ts).await?;
+                drawdown(month, tasks, owner)?;
+
+                Ok(())
+            }
         },
         None => {
             let task_ids = tau.get_ids().await?;

+ 10 - 4
bin/tau/tau-cli/src/primitives.rs

@@ -56,7 +56,7 @@ pub struct BaseTask {
     pub rank: Option<f32>,
 }
 
-#[derive(serde::Serialize, serde::Deserialize, Debug)]
+#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)]
 pub struct TaskInfo {
     pub ref_id: String,
     pub workspace: String,
@@ -69,13 +69,15 @@ pub struct TaskInfo {
     pub due: Option<i64>,
     pub rank: f32,
     pub created_at: i64,
+    pub state: String,
     pub events: Vec<TaskEvent>,
     pub comments: Vec<Comment>,
 }
 
-#[derive(serde::Serialize, serde::Deserialize, Debug)]
+#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)]
 pub struct TaskEvent {
     pub action: String,
+    pub content: String,
     pub timestamp: Timestamp,
 }
 
@@ -87,11 +89,15 @@ impl std::fmt::Display for TaskEvent {
 
 impl Default for TaskEvent {
     fn default() -> Self {
-        Self { action: State::Open.to_string(), timestamp: Timestamp::current_time() }
+        Self {
+            action: State::Open.to_string(),
+            content: "".to_string(),
+            timestamp: Timestamp::current_time(),
+        }
     }
 }
 
-#[derive(serde::Serialize, serde::Deserialize, Debug)]
+#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)]
 pub struct Comment {
     content: String,
     author: String,

+ 9 - 1
bin/tau/tau-cli/src/rpc.rs

@@ -22,7 +22,7 @@ impl Tau {
         Ok(())
     }
 
-    /// Get all task ids.
+    /// Get current open tasks ids.
     pub async fn get_ids(&self) -> Result<Vec<u64>> {
         let req = JsonRequest::new("get_ids", json!([]));
         let rep = self.rpc_client.request(req).await?;
@@ -70,6 +70,14 @@ impl Tau {
         Ok(serde_json::from_value(rep)?)
     }
 
+    /// Get month's stopped tasks.
+    pub async fn get_stop_tasks(&self, month: i64) -> Result<Vec<TaskInfo>> {
+        let req = JsonRequest::new("get_stop_tasks", json!([month]));
+        let rep = self.rpc_client.request(req).await?;
+
+        Ok(serde_json::from_value(rep)?)
+    }
+
     /// Switch workspace.
     pub async fn switch_ws(&self, workspace: String) -> Result<()> {
         let req = JsonRequest::new("switch_ws", json!([workspace]));

+ 18 - 6
bin/tau/tau-cli/src/view.rs

@@ -53,8 +53,7 @@ pub fn print_task_list(tasks: Vec<TaskInfo>, filters: Vec<String>) -> Result<()>
     };
 
     for task in tasks {
-        let state = task.events.last().unwrap_or(&TaskEvent::default()).action.clone();
-        let state = State::from_str(&state)?;
+        let state = State::from_str(&task.state.clone())?;
 
         let (max_style, min_style, mid_style, gen_style) = if state.is_start() {
             ("bFg", "Fc", "Fg", "Fg")
@@ -83,7 +82,7 @@ pub fn print_task_list(tasks: Vec<TaskInfo>, filters: Vec<String>) -> Result<()>
         ]));
     }
 
-    let mut ws_table = table!([Fb => workspace]);
+    let mut ws_table = table!([workspace]);
     ws_table.set_format(
         FormatBuilder::new()
             .padding(1, 1)
@@ -97,7 +96,6 @@ pub fn print_task_list(tasks: Vec<TaskInfo>, filters: Vec<String>) -> Result<()>
 }
 
 pub fn print_task_info(taskinfo: TaskInfo) -> Result<()> {
-    let current_state = &taskinfo.events.last().unwrap_or(&TaskEvent::default()).action.clone();
     let due = timestamp_to_date(taskinfo.due.unwrap_or(0), DateFormat::Date);
     let created_at = timestamp_to_date(taskinfo.created_at, DateFormat::DateTime);
 
@@ -113,7 +111,7 @@ pub fn print_task_info(taskinfo: TaskInfo) -> Result<()> {
         [Bd =>"due", due],
         ["rank", &taskinfo.rank.to_string()],
         [Bd =>"created_at", created_at],
-        ["current_state", current_state]);
+        ["current_state", &taskinfo.state]);
 
     table.set_format(
         FormatBuilder::new()
@@ -144,7 +142,21 @@ pub fn comments_as_string(comments: Vec<Comment>) -> String {
 pub fn events_as_string(events: Vec<TaskEvent>) -> String {
     let mut events_str = String::new();
     for event in events {
-        writeln!(events_str, "State changed to {} at {}", event.action, event.timestamp).unwrap();
+        match event.action.as_str() {
+            "state" => {
+                writeln!(events_str, "State changed to {} at {}", event.content, event.timestamp)
+                    .unwrap()
+            }
+            "assign" => {
+                writeln!(events_str, "Assigned to {} at {}", event.content, event.timestamp)
+                    .unwrap();
+            }
+            "comment" => {
+                writeln!(events_str, "{} added a comment at {}", event.content, event.timestamp)
+                    .unwrap();
+            }
+            _ => {}
+        }
     }
     events_str
 }

+ 29 - 6
bin/tau/taud/src/jsonrpc.rs

@@ -60,6 +60,7 @@ impl RequestHandler for JsonRpcInterface {
             Some("switch_ws") => self.switch_ws(params).await,
             Some("export") => self.export_to(params).await,
             Some("import") => self.import_from(params).await,
+            Some("get_stop_tasks") => self.get_stop_tasks(params).await,
             Some(_) | None => return JsonError::new(ErrorCode::MethodNotFound, None, req.id).into(),
         };
 
@@ -121,7 +122,7 @@ impl JsonRpcInterface {
     async fn get_ids(&self, params: &[Value]) -> TaudResult<Value> {
         debug!(target: "tau", "JsonRpc::get_ids() params {:?}", params);
         let ws = self.workspace.lock().await.clone();
-        let tasks = MonthTasks::load_current_open_tasks(&self.dataset_path, ws)?;
+        let tasks = MonthTasks::load_current_tasks(&self.dataset_path, ws, false)?;
         let task_ids: Vec<u32> = tasks.iter().map(|task| task.get_id()).collect();
         Ok(json!(task_ids))
     }
@@ -210,6 +211,27 @@ impl JsonRpcInterface {
         Ok(json!(task))
     }
 
+    // RPCAPI:
+    // Get all tasks.
+    // --> {"jsonrpc": "2.0", "method": "get_all_tasks", "params": [task_id], "id": 1}
+    // <-- {"jsonrpc": "2.0", "result": "task", "id": 1}
+    async fn get_stop_tasks(&self, params: &[Value]) -> TaudResult<Value> {
+        debug!(target: "tau", "JsonRpc::get_all_tasks() params {:?}", params);
+
+        if params.len() != 1 {
+            return Err(TaudError::InvalidData("len of params should be 1".into()))
+        }
+        if !params[0].is_i64() {
+            return Err(TaudError::InvalidData("Invalid month".into()))
+        }
+        let month = Timestamp(params[0].as_i64().unwrap());
+        let ws = self.workspace.lock().await.clone();
+
+        let tasks = MonthTasks::load_stop_tasks(&self.dataset_path, ws, &month)?;
+
+        Ok(json!(tasks))
+    }
+
     // RPCAPI:
     // Switch tasks workspace.
     // --> {"jsonrpc": "2.0", "method": "switch_ws", "params": [workspace], "id": 1}
@@ -252,12 +274,13 @@ impl JsonRpcInterface {
             return Err(TaudError::InvalidData("Invalid path".into()))
         }
 
+        let ws = self.workspace.lock().await.clone();
         let path = expand_path(params[0].as_str().unwrap())?.join("exported_tasks");
         // mkdir datastore_path if not exists
         create_dir_all(path.join("month")).map_err(Error::from)?;
         create_dir_all(path.join("task")).map_err(Error::from)?;
-        let mt = MonthTasks::load_or_create(None, &self.dataset_path)?;
-        let tasks = mt.objects(&self.dataset_path)?;
+
+        let tasks = MonthTasks::load_current_tasks(&self.dataset_path, ws, true)?;
 
         for task in tasks {
             task.save(&path)?;
@@ -281,9 +304,9 @@ impl JsonRpcInterface {
             return Err(TaudError::InvalidData("Invalid path".into()))
         }
 
+        let ws = self.workspace.lock().await.clone();
         let path = expand_path(params[0].as_str().unwrap())?.join("exported_tasks");
-        let mt = MonthTasks::load_or_create(None, &path)?;
-        let tasks = mt.objects(&path)?;
+        let tasks = MonthTasks::load_current_tasks(&path, ws, true)?;
 
         for task in tasks {
             self.notify_queue_sender.send(task).await.map_err(Error::from)?;
@@ -293,7 +316,7 @@ impl JsonRpcInterface {
 
     fn load_task_by_id(&self, task_id: &Value, ws: String) -> TaudResult<TaskInfo> {
         let task_id: u64 = serde_json::from_value(task_id.clone())?;
-        let tasks = MonthTasks::load_current_open_tasks(&self.dataset_path, ws)?;
+        let tasks = MonthTasks::load_current_tasks(&self.dataset_path, ws, false)?;
         let task = tasks.into_iter().find(|t| (t.get_id() as u64) == task_id);
 
         task.ok_or(TaudError::InvalidId)

+ 57 - 18
bin/tau/taud/src/month_tasks.rs

@@ -20,18 +20,23 @@ use crate::{
 #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
 pub struct MonthTasks {
     created_at: Timestamp,
-    task_tks: Vec<String>,
+    active_tks: Vec<String>,
+    deactive_tks: Vec<String>,
 }
 
 impl MonthTasks {
-    pub fn new(task_tks: &[String]) -> Self {
-        Self { created_at: Timestamp::current_time(), task_tks: task_tks.to_owned() }
+    pub fn new(active_tks: &[String], deactive_tks: &[String]) -> Self {
+        Self {
+            created_at: Timestamp::current_time(),
+            active_tks: active_tks.to_owned(),
+            deactive_tks: deactive_tks.to_owned(),
+        }
     }
 
     pub fn add(&mut self, ref_id: &str) {
         debug!(target: "tau", "MonthTasks::add()");
-        if !self.task_tks.contains(&ref_id.into()) {
-            self.task_tks.push(ref_id.into());
+        if !self.active_tks.contains(&ref_id.into()) {
+            self.active_tks.push(ref_id.into());
         }
     }
 
@@ -39,7 +44,11 @@ impl MonthTasks {
         debug!(target: "tau", "MonthTasks::objects()");
         let mut tks: Vec<TaskInfo> = vec![];
 
-        for ref_id in self.task_tks.iter() {
+        for ref_id in self.active_tks.iter() {
+            tks.push(TaskInfo::load(ref_id, dataset_path)?);
+        }
+
+        for ref_id in self.deactive_tks.iter() {
             tks.push(TaskInfo::load(ref_id, dataset_path)?);
         }
 
@@ -48,8 +57,12 @@ impl MonthTasks {
 
     pub fn remove(&mut self, ref_id: &str) {
         debug!(target: "tau", "MonthTasks::remove()");
-        if let Some(index) = self.task_tks.iter().position(|t| *t == ref_id) {
-            self.task_tks.remove(index);
+        if self.active_tks.contains(&ref_id.to_string()) {
+            if let Some(index) = self.active_tks.iter().position(|t| *t == ref_id) {
+                self.deactive_tks.push(self.active_tks.remove(index));
+            }
+        } else {
+            self.deactive_tks.push(ref_id.to_owned());
         }
     }
 
@@ -84,7 +97,7 @@ impl MonthTasks {
     fn create(date: &Timestamp, dataset_path: &Path) -> TaudResult<Self> {
         debug!(target: "tau", "MonthTasks::create()");
 
-        let mut mt = Self::new(&[]);
+        let mut mt = Self::new(&[], &[]);
         mt.set_date(date);
         mt.save(dataset_path)?;
         Ok(mt)
@@ -106,14 +119,19 @@ impl MonthTasks {
                     Err(_) => vec![],
                 };
 
-                let mut loaded_mt = Self::new(&[]);
+                let mut loaded_mt = Self::new(&[], &[]);
 
                 for path in path_all {
                     let mt = load_json_file::<Self>(&path)?;
                     loaded_mt.created_at = mt.created_at;
-                    for tks in mt.task_tks {
-                        if !loaded_mt.task_tks.contains(&tks) {
-                            loaded_mt.task_tks.push(tks)
+                    for tks in mt.active_tks {
+                        if !loaded_mt.active_tks.contains(&tks) {
+                            loaded_mt.active_tks.push(tks)
+                        }
+                    }
+                    for dtks in mt.deactive_tks {
+                        if !loaded_mt.deactive_tks.contains(&dtks) {
+                            loaded_mt.deactive_tks.push(dtks)
                         }
                     }
                 }
@@ -122,13 +140,34 @@ impl MonthTasks {
         }
     }
 
-    pub fn load_current_open_tasks(dataset_path: &Path, ws: String) -> TaudResult<Vec<TaskInfo>> {
+    pub fn load_current_tasks(
+        dataset_path: &Path,
+        ws: String,
+        all: bool,
+    ) -> TaudResult<Vec<TaskInfo>> {
         let mt = Self::load_or_create(None, dataset_path)?;
+
+        if all {
+            Ok(mt.objects(dataset_path)?.into_iter().filter(|t| t.workspace == ws).collect())
+        } else {
+            Ok(mt
+                .objects(dataset_path)?
+                .into_iter()
+                .filter(|t| t.get_state() != "stop" && t.workspace == ws)
+                .collect())
+        }
+    }
+
+    pub fn load_stop_tasks(
+        dataset_path: &Path,
+        ws: String,
+        date: &Timestamp,
+    ) -> TaudResult<Vec<TaskInfo>> {
+        let mt = Self::load_or_create(Some(date), dataset_path)?;
         Ok(mt
             .objects(dataset_path)?
             .into_iter()
-            .filter(|t| t.get_state() != "stop")
-            .filter(|t| t.workspace == ws)
+            .filter(|t| t.get_state() == "stop" && t.workspace == ws)
             .collect())
     }
 }
@@ -192,7 +231,7 @@ mod tests {
 
         let task_tks = vec![];
 
-        let mut mt = MonthTasks::new(&task_tks);
+        let mut mt = MonthTasks::new(&task_tks, &[]);
 
         mt.save(&dataset_path)?;
 
@@ -225,7 +264,7 @@ mod tests {
 
         let mt_load = MonthTasks::load_or_create(Some(&Timestamp::current_time()), &dataset_path)?;
 
-        assert!(mt_load.task_tks.contains(&task.ref_id));
+        assert!(mt_load.active_tks.contains(&task.ref_id));
 
         remove_dir_all(TEST_DATA_PATH).ok();
 

+ 22 - 13
bin/tau/taud/src/task_info.rs

@@ -22,12 +22,13 @@ use crate::{
 #[derive(Clone, Debug, Serialize, Deserialize, SerialEncodable, SerialDecodable, PartialEq, Eq)]
 struct TaskEvent {
     action: String,
+    content: String,
     timestamp: Timestamp,
 }
 
 impl TaskEvent {
-    fn new(action: String) -> Self {
-        Self { action, timestamp: Timestamp::current_time() }
+    fn new(action: String, content: String) -> Self {
+        Self { action, content, timestamp: Timestamp::current_time() }
     }
 }
 
@@ -70,6 +71,7 @@ pub struct TaskInfo {
     due: Option<Timestamp>,
     rank: f32,
     created_at: Timestamp,
+    state: String,
     events: TaskEvents,
     comments: TaskComments,
 }
@@ -90,7 +92,7 @@ impl TaskInfo {
         let created_at = Timestamp::current_time();
 
         let task_ids: Vec<u32> =
-            MonthTasks::load_current_open_tasks(dataset_path, workspace.clone())?
+            MonthTasks::load_current_tasks(dataset_path, workspace.clone(), false)?
                 .into_iter()
                 .map(|t| t.id)
                 .collect();
@@ -115,6 +117,7 @@ impl TaskInfo {
             due,
             rank,
             created_at,
+            state: "open".into(),
             comments: TaskComments(vec![]),
             events: TaskEvents(vec![]),
         })
@@ -156,14 +159,10 @@ impl TaskInfo {
 
     pub fn get_state(&self) -> String {
         debug!(target: "tau", "TaskInfo::get_state()");
-        if let Some(ev) = self.events.0.last() {
-            ev.action.clone()
-        } else {
-            "open".into()
-        }
+        self.state.clone()
     }
 
-    fn get_path(ref_id: &str, dataset_path: &Path) -> PathBuf {
+    pub fn get_path(ref_id: &str, dataset_path: &Path) -> PathBuf {
         debug!(target: "tau", "TaskInfo::get_path()");
         dataset_path.join("task").join(ref_id)
     }
@@ -186,6 +185,7 @@ impl TaskInfo {
     pub fn set_assign(&mut self, assign: &[String]) {
         debug!(target: "tau", "TaskInfo::set_assign()");
         self.assign = TaskAssigns(assign.to_owned());
+        self.set_event("assign", &assign.join(", "));
     }
 
     pub fn set_project(&mut self, project: &[String]) {
@@ -195,7 +195,8 @@ impl TaskInfo {
 
     pub fn set_comment(&mut self, c: Comment) {
         debug!(target: "tau", "TaskInfo::set_comment()");
-        self.comments.0.push(c);
+        self.comments.0.push(c.clone());
+        self.set_event("comment", &c.author);
     }
 
     pub fn set_rank(&mut self, r: f32) {
@@ -208,12 +209,20 @@ impl TaskInfo {
         self.due = d;
     }
 
-    pub fn set_state(&mut self, action: &str) {
+    pub fn set_event(&mut self, action: &str, content: &str) {
+        debug!(target: "tau", "TaskInfo::set_event()");
+        if !content.is_empty() {
+            self.events.0.push(TaskEvent::new(action.into(), content.into()));
+        }
+    }
+
+    pub fn set_state(&mut self, state: &str) {
         debug!(target: "tau", "TaskInfo::set_state()");
-        if self.get_state() == action {
+        if self.get_state() == state {
             return
         }
-        self.events.0.push(TaskEvent::new(action.into()));
+        self.state = state.to_string();
+        self.set_event("state", state);
     }
 }
 

+ 105 - 0
script/research/ec/divisor.sage

@@ -0,0 +1,105 @@
+DIV_POINT = 1
+DIV_FUNC  = 2
+
+class Divisor:
+
+    def __init__(self, field):
+        self.field = field
+        self._div = []
+
+    def __call__(self, Px, Py, Pz=1):
+        K = self.field
+        # Convert to base field
+        Px, Py, Pz = K(Px), K(Py), K(Pz)
+        # Normalize coordinates
+        if Pz > 0:
+            Px /= Pz
+            Py /= Pz
+            Pz = 1
+        P = (Px, Py, Pz)
+
+        D = Divisor(K)
+        D._div += [(DIV_POINT, 1, P)]
+        return D
+
+    def div(self, f):
+        K = self.field
+        D = Divisor(K)
+        D._div += [(DIV_FUNC, 1, f)]
+        return D
+
+    def _clean(self):
+        self._div = [(type_id, self._deg_obj(P), P)
+                     for type_id, P in self._objs()]
+        self._div = [(type_id, n, P) for type_id, n, P in self._div if n != 0]
+    def _deg_obj(self, P):
+        return sum(n for type_id, n, Q in self._div if P == Q)
+    def _objs(self):
+        return set((type_id, P) for type_id, _, P in self._div)
+
+    def __add__(self, other):
+        K = self.field
+        D = Divisor(K)
+        D._div = self._div[:] + other._div[:]
+        D._clean()
+        return D
+
+    def __sub__(self, other):
+        K = self.field
+        D = self + -1*other
+        return D
+
+    def __mul__(self, n):
+        K = self.field
+        D = Divisor(K)
+        D._div = [(type_id, n*m, P) for type_id, m, P in self._div]
+        return D
+
+    __rmul__ = __mul__
+
+    def __str__(self):
+        out = ""
+        if not self._div:
+            out += "0"
+        for i, (type_id, n, obj) in enumerate(self._div):
+            assert n != 0
+            if i > 0:
+                if n > 0:
+                    out += " + "
+                else:
+                    out += " - "
+            assert type_id in (DIV_POINT, DIV_FUNC)
+            if type_id == DIV_POINT:
+                out += f"{abs(n)}" + self._format_point(obj)
+            elif type_id == DIV_FUNC:
+                out += f"{abs(n)} div({obj})"
+        return out
+
+    def _format_point(self, P):
+        Px, Py, Pz = P
+        assert Pz in (0, 1)
+        if Pz == 0:
+            return f"[∞]"
+        return f"[({Px}, {Py})]"
+
+    def deg(self):
+        return sum(n for _, n, _ in self._div)
+
+    def supp(self):
+        return set(P for _, _, P in self._div)
+
+K.<x, y> = GF(47)[]
+D = Divisor(K)
+D = 4*D(2, 3) + D(2, 4) + 2*D(0, 1, 0) + 4*D.div(x^2 + y)
+E = 6*D(4, 2) - 6*D(6, 3) + 6*D(6, 3)
+#D -= 4*D.div(x^2 + y)
+#E -= 6*D(4, 2)
+print(f"D = {D}")
+print(f"E = {E}")
+print(f"D + E = {D + E}")
+print(f"6E = {6*E}")
+print(f"deg(D) = {D.deg()}")
+print(f"supp(D) = {D.supp()}")
+print(f"deg(E) = {E.deg()}")
+print(f"supp(E) = {E.supp()}")
+

+ 13 - 5
src/zk/gadget/less_than.rs

@@ -113,6 +113,7 @@ impl<const WINDOW_SIZE: usize, const NUM_OF_BITS: usize, const NUM_OF_WINDOWS: u
         a: Value<pallas::Base>,
         b: Value<pallas::Base>,
         offset: usize,
+        strict: bool,
     ) -> Result<(), Error> {
         let (a, _, a_offset) = layouter.assign_region(
             || "a less than b",
@@ -124,7 +125,7 @@ impl<const WINDOW_SIZE: usize, const NUM_OF_BITS: usize, const NUM_OF_WINDOWS: u
             },
         )?;
 
-        self.less_than_range_check(layouter, a, a_offset)?;
+        self.less_than_range_check(layouter, a, a_offset, strict)?;
 
         Ok(())
     }
@@ -135,6 +136,7 @@ impl<const WINDOW_SIZE: usize, const NUM_OF_BITS: usize, const NUM_OF_WINDOWS: u
         a: AssignedCell<pallas::Base, pallas::Base>,
         b: AssignedCell<pallas::Base, pallas::Base>,
         offset: usize,
+        strict: bool,
     ) -> Result<(), Error> {
         let (a, _, a_offset) = layouter.assign_region(
             || "a less than b",
@@ -146,7 +148,7 @@ impl<const WINDOW_SIZE: usize, const NUM_OF_BITS: usize, const NUM_OF_WINDOWS: u
             },
         )?;
 
-        self.less_than_range_check(layouter, a, a_offset)?;
+        self.less_than_range_check(layouter, a, a_offset, strict)?;
 
         Ok(())
     }
@@ -156,6 +158,7 @@ impl<const WINDOW_SIZE: usize, const NUM_OF_BITS: usize, const NUM_OF_WINDOWS: u
         mut layouter: impl Layouter<pallas::Base>,
         a: AssignedCell<pallas::Base, pallas::Base>,
         a_offset: AssignedCell<pallas::Base, pallas::Base>,
+        strict: bool,
     ) -> Result<(), Error> {
         let range_a_chip =
             NativeRangeCheckChip::<WINDOW_SIZE, NUM_OF_BITS, NUM_OF_WINDOWS>::construct(
@@ -166,9 +169,13 @@ impl<const WINDOW_SIZE: usize, const NUM_OF_BITS: usize, const NUM_OF_WINDOWS: u
                 self.config.range_a_offset_config.clone(),
             );
 
-        range_a_chip.copy_range_check(layouter.namespace(|| "a copy_range_check"), a)?;
-        range_a_offset_chip
-            .copy_range_check(layouter.namespace(|| "a_offset copy_range_check"), a_offset)?;
+        range_a_chip.copy_range_check(layouter.namespace(|| "a copy_range_check"), a, strict)?;
+
+        range_a_offset_chip.copy_range_check(
+            layouter.namespace(|| "a_offset copy_range_check"),
+            a_offset,
+            strict,
+        )?;
 
         Ok(())
     }
@@ -265,6 +272,7 @@ mod tests {
                         self.a,
                         self.b,
                         0,
+                        true,
                     )?;
 
                     Ok(())

+ 17 - 6
src/zk/gadget/native_range_check.rs

@@ -114,12 +114,12 @@ impl<const WINDOW_SIZE: usize, const NUM_BITS: usize, const NUM_WINDOWS: usize>
             .collect()
     }
 
-    // TODO: strict bool
     pub fn decompose(
         &self,
         region: &mut Region<'_, pallas::Base>,
         z_0: AssignedCell<pallas::Base, pallas::Base>,
         offset: usize,
+        strict: bool,
     ) -> Result<(), plonk::Error> {
         assert!(WINDOW_SIZE * NUM_WINDOWS < NUM_BITS + WINDOW_SIZE);
 
@@ -155,7 +155,12 @@ impl<const WINDOW_SIZE: usize, const NUM_BITS: usize, const NUM_WINDOWS: usize>
         }
 
         assert!(z_values.len() == NUM_WINDOWS + 1);
-        region.constrain_constant(z_values.last().unwrap().cell(), pallas::Base::zero())?;
+
+        if strict {
+            // Constrain the remaining bits to be zero
+            region.constrain_constant(z_values.last().unwrap().cell(), pallas::Base::zero())?;
+        }
+
         Ok(())
     }
 
@@ -163,12 +168,13 @@ impl<const WINDOW_SIZE: usize, const NUM_BITS: usize, const NUM_WINDOWS: usize>
         &self,
         mut layouter: impl Layouter<pallas::Base>,
         value: Value<pallas::Base>,
+        strict: bool,
     ) -> Result<(), plonk::Error> {
         layouter.assign_region(
             || format!("witness {}-bit native range check", NUM_BITS),
             |mut region: Region<'_, pallas::Base>| {
                 let z_0 = region.assign_advice(|| "z_0", self.config.z, 0, || value)?;
-                self.decompose(&mut region, z_0, 0)?;
+                self.decompose(&mut region, z_0, 0, strict)?;
                 Ok(())
             },
         )
@@ -178,12 +184,13 @@ impl<const WINDOW_SIZE: usize, const NUM_BITS: usize, const NUM_WINDOWS: usize>
         &self,
         mut layouter: impl Layouter<pallas::Base>,
         value: AssignedCell<pallas::Base, pallas::Base>,
+        strict: bool,
     ) -> Result<(), plonk::Error> {
         layouter.assign_region(
             || format!("copy {}-bit native range check", NUM_BITS),
             |mut region: Region<'_, pallas::Base>| {
                 let z_0 = value.copy_advice(|| "z_0", &mut region, self.config.z, 0)?;
-                self.decompose(&mut region, z_0, 0)?;
+                self.decompose(&mut region, z_0, 0, strict)?;
                 Ok(())
             },
         )
@@ -251,12 +258,16 @@ mod tests {
                     )?;
 
                     let a = assign_free_advice(layouter.namespace(|| "load a"), config.1, self.a)?;
-                    rangecheck_chip
-                        .copy_range_check(layouter.namespace(|| "copy a and range check"), a)?;
+                    rangecheck_chip.copy_range_check(
+                        layouter.namespace(|| "copy a and range check"),
+                        a,
+                        true,
+                    )?;
 
                     rangecheck_chip.witness_range_check(
                         layouter.namespace(|| "witness a and range check"),
                         self.a,
+                        true,
                     )?;
 
                     Ok(())