mirror of
https://github.com/darkrenaissance/darkfi.git
synced 2026-04-28 03:00:18 -04:00
tau: rework on tau-cli functionalities and design
This commit is contained in:
@@ -32,16 +32,6 @@ seed node shouldn't be advertised in the list of connectable nodes. The seed
|
||||
node does not participate as a normal node in the p2p network. It simply allows
|
||||
new nodes to discover other nodes in the network during the bootstrapping phase.
|
||||
|
||||
Also note that for the first time ever running seed node you must run it with
|
||||
`--key-gen`:
|
||||
```shell
|
||||
% taud --key-gen
|
||||
```
|
||||
This will generate a new secret key in `/home/${USER}/.config/tau/secret_key` that
|
||||
you can share with nodes you want them to get and decrypt your tasks, otherwise if you
|
||||
have already generated or got a copy from a peer place it in the same directory
|
||||
`/home/${USER}/.config/tau/secret_key`.
|
||||
|
||||
### Inbound Node
|
||||
|
||||
This is a node accepting inbound connections on the network but which is not
|
||||
@@ -74,175 +64,81 @@ connect to in the p2p network.
|
||||
## Seed nodes to connect to
|
||||
seeds=["127.0.0.1:11001"]
|
||||
|
||||
|
||||
Also note that for the first time ever running seed node you must run it with
|
||||
`--key-gen`:
|
||||
```shell
|
||||
% taud --key-gen
|
||||
```
|
||||
This will generate a new secret key in `/home/${USER}/.config/tau/secret_key` that
|
||||
you can share with nodes you want them to get and decrypt your tasks, otherwise if you
|
||||
have already generated or got a copy from a peer place it in the same directory
|
||||
`/home/${USER}/.config/tau/secret_key`.
|
||||
|
||||
|
||||
## Usage (CLI)
|
||||
|
||||
```shell
|
||||
% tau --help
|
||||
```
|
||||
|
||||
tau 0.3.0
|
||||
Tau cli
|
||||
|
||||
|
||||
USAGE:
|
||||
tau [FLAGS] [OPTIONS] [ARGS] [SUBCOMMAND]
|
||||
|
||||
tau [FLAGS] [OPTIONS] [ARGS] [SUBCOMMAND]
|
||||
|
||||
FLAGS:
|
||||
-h, --help Prints help information
|
||||
-V, --version Prints version information
|
||||
-v Increase verbosity
|
||||
|
||||
-h, --help Prints help information
|
||||
-V, --version Prints version information
|
||||
-v Increase verbosity
|
||||
|
||||
OPTIONS:
|
||||
-c, --config <config> Sets a custom config file
|
||||
--rpc <rpc-listen> JSON-RPC listen URL [default: 127.0.0.1:11055]
|
||||
|
||||
-c, --config <config> Sets a custom config file
|
||||
--rpc <rpc-listen> JSON-RPC listen URL [default: 127.0.0.1:11055]
|
||||
|
||||
ARGS:
|
||||
<id> Get task by ID
|
||||
<filters>... Search criteria (zero or more)
|
||||
|
||||
<id> Get task by ID
|
||||
<filters>... Search criteria (zero or more)
|
||||
|
||||
SUBCOMMANDS:
|
||||
add Add a new task
|
||||
comment Set or Get comment for a task
|
||||
help Prints this message or the help of the given subcommand(s)
|
||||
list List all tasks
|
||||
state Set or Get task state
|
||||
update Update/Edit an existing task by ID
|
||||
add Add a new task
|
||||
comment Set or Get comment for a task
|
||||
help Prints this message or the help of the given subcommand(s)
|
||||
list List all tasks
|
||||
state Set or Get task state
|
||||
update Update/Edit an existing task by ID
|
||||
|
||||
```shell
|
||||
% tau help [SUBCOMMAND]
|
||||
```
|
||||
|
||||
### Add new tasks
|
||||
### Example
|
||||
|
||||
```shell
|
||||
% tau add title1 description person1,person2 project1,project2 0405 4.74
|
||||
% tau add title2 "some description" person1 project1 0805 18
|
||||
% # this will prompt terminal for title
|
||||
% tau add
|
||||
Title: new title
|
||||
% # then your system's default editor will open up and you could write some description
|
||||
% # you should have/add environment variable EDITOR pointing to your favorite text editor
|
||||
```
|
||||
for more information:
|
||||
```shell
|
||||
% tau add --help
|
||||
$ # add new task
|
||||
$ tau add "new title"
|
||||
$ tau add "new title" project:blockchain desc:"new description" rank:3 assign:dark
|
||||
$
|
||||
$ # lists tasks
|
||||
$ tau
|
||||
$ tau open # open tasks
|
||||
$ tau pause # paused tasks
|
||||
$ tau 0522 # created at May 2022
|
||||
$ tau project:blockchain assign:dark
|
||||
$ tau rank:gt:n # lists all tasks that have rank greater than n
|
||||
$ tau rank:ls:n # lists all tasks that have rank lesser than n
|
||||
$
|
||||
$ # update task
|
||||
$ tau update 3 project:network rank:20
|
||||
$
|
||||
$ # state
|
||||
$ tau state 3 # get state
|
||||
$ tau state 3 pause # set the state to pause
|
||||
$
|
||||
$ # comments
|
||||
$ tau comments 1 # list comments
|
||||
$ tau comments 3 "new comment" # add new comment
|
||||
```
|
||||
|
||||
|
||||
### List existing tasks
|
||||
|
||||
```shell
|
||||
% tau list # or just tau
|
||||
```
|
||||
Output:
|
||||
```text
|
||||
ID | Title | Project | Assigned | Due | Rank
|
||||
----+-----------+-------------------+-----------------+-----------------+------
|
||||
2 | title2 | project1 | person1 | Sunday 8 May | 18
|
||||
1 | title1 | project1,project2 | person1,person2 | Wednesday 4 May | 4.74
|
||||
3 | new title | | | | 0
|
||||
```
|
||||
|
||||
|
||||
### List tasks with filters
|
||||
|
||||
```shell
|
||||
% tau # lists all tasks
|
||||
% tau open # lists currently open tasks
|
||||
% tau pause # lists currently paused tasks
|
||||
% tau 0522 # lists tasks created at May 2022
|
||||
% tau project:value # lists all tasks that have "value" in their Project
|
||||
% tau assign:value # lists all tasks that have "value" in their Assign
|
||||
% tau "rank>number" # lists all tasks that have rank greater than "number"
|
||||
% tau "rank<number" # lists all tasks that have rank lesser than "number"
|
||||
```
|
||||
|
||||
Combined filters:
|
||||
```shell
|
||||
% tau project:project1 assign:person2 0522 open
|
||||
```
|
||||
Output:
|
||||
```text
|
||||
ID | Title | Project | Assigned | Due | Rank
|
||||
----+--------+----------+-----------------+-----------------+------
|
||||
1 | title1 | project1 | person1,person2 | Wednesday 4 May | 4.74
|
||||
```
|
||||
|
||||
|
||||
### Update an existing task
|
||||
|
||||
```shell
|
||||
% tau update 3 project project3
|
||||
% tau "rank<4" # qoutes are for escaping special characters
|
||||
```
|
||||
Output:
|
||||
```text
|
||||
ID | Title | Project | Assigned | Due | Rank
|
||||
----+-----------+----------+----------+-----+------
|
||||
3 | new title | project3 | | | 0
|
||||
```
|
||||
|
||||
|
||||
### Get/Set task state
|
||||
|
||||
```shell
|
||||
% tau state 1
|
||||
```
|
||||
Output:
|
||||
```text
|
||||
Task with id 1 is: "open"
|
||||
```
|
||||
|
||||
```shell
|
||||
% tau state 1 pause
|
||||
% tau state 1
|
||||
```
|
||||
Output:
|
||||
```text
|
||||
Task with id 1 is: "pause"
|
||||
```
|
||||
|
||||
```shell
|
||||
% tau state 2 stop # this will deactivate the task (task is done)
|
||||
```
|
||||
|
||||
|
||||
### Get/Set comment
|
||||
|
||||
```shell
|
||||
% tau comment 1 person1 "some awesome comment"
|
||||
% tau comment 1 person2 "other awesome comment"
|
||||
% tau comment 1
|
||||
```
|
||||
Output:
|
||||
```text
|
||||
Comments on Task with id 1:
|
||||
person1: some awesome comment
|
||||
person2: other awesome comment
|
||||
```
|
||||
|
||||
|
||||
### Get a task
|
||||
|
||||
```shell
|
||||
% tau 1
|
||||
```
|
||||
Output:
|
||||
```text
|
||||
Name | Value
|
||||
---------------+--------------------------------
|
||||
ref_id | cGw1AI7cBSdJWIqPMU8d355wRrB0qy
|
||||
id | 1
|
||||
title | title1
|
||||
desc | description
|
||||
assign | person1,person2
|
||||
project | project1
|
||||
due | Wednesday 4 May
|
||||
rank | 4.74
|
||||
created_at | 21:28 Monday 2 May
|
||||
current_state | pause
|
||||
comments | person1: some awesome comment
|
||||
| person2: other awesome comment
|
||||
------------------------------------------------------
|
||||
events State changed to pause at 21:34 Monday 2 May
|
||||
------------------------------------------------------
|
||||
```
|
||||
112
bin/tau/tau-cli/src/cli.rs
Normal file
112
bin/tau/tau-cli/src/cli.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use structopt::StructOpt;
|
||||
use structopt_toml::StructOptToml;
|
||||
|
||||
use darkfi::Result;
|
||||
|
||||
use super::util::due_as_timestamp;
|
||||
|
||||
/// Tau cli
|
||||
#[derive(Debug, Deserialize, StructOpt, StructOptToml)]
|
||||
#[serde(default)]
|
||||
#[structopt(name = "tau")]
|
||||
pub struct CliTau {
|
||||
/// Increase verbosity
|
||||
#[structopt(short, parse(from_occurrences))]
|
||||
pub verbose: u8,
|
||||
/// JSON-RPC listen URL
|
||||
#[structopt(long = "rpc", default_value = "127.0.0.1:11055")]
|
||||
pub rpc_listen: SocketAddr,
|
||||
/// Sets a custom config file
|
||||
#[structopt(short, long)]
|
||||
pub config: Option<String>,
|
||||
#[structopt(subcommand)]
|
||||
pub command: Option<CliTauSubCommands>,
|
||||
/// Get task by ID
|
||||
pub id: Option<String>,
|
||||
/// Search criteria (zero or more)
|
||||
#[structopt(multiple = true)]
|
||||
pub filters: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(StructOpt, Deserialize, Debug)]
|
||||
pub enum CliTauSubCommands {
|
||||
/// Add a new task
|
||||
Add { values: Vec<String> },
|
||||
/// Update/Edit an existing task by ID
|
||||
Update {
|
||||
/// Task ID
|
||||
id: u64,
|
||||
/// Values (ex: project:blockchain)
|
||||
values: Vec<String>,
|
||||
},
|
||||
/// Set or Get task state
|
||||
State {
|
||||
/// Task ID
|
||||
id: u64,
|
||||
/// Set task state
|
||||
state: Option<String>,
|
||||
},
|
||||
/// Set or Get comment for a task
|
||||
Comment {
|
||||
/// Task ID
|
||||
id: u64,
|
||||
/// Comment content
|
||||
content: Option<String>,
|
||||
},
|
||||
/// List all tasks
|
||||
List {},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct CliBaseTask {
|
||||
pub title: String,
|
||||
pub desc: Option<String>,
|
||||
pub assign: Vec<String>,
|
||||
pub project: Vec<String>,
|
||||
pub due: Option<i64>,
|
||||
pub rank: Option<f32>,
|
||||
}
|
||||
|
||||
pub fn task_from_cli_values(values: Vec<String>) -> Result<CliBaseTask> {
|
||||
let mut title: String = String::new();
|
||||
let mut desc: Option<String> = None;
|
||||
let mut project: Vec<String> = vec![];
|
||||
let mut assign: Vec<String> = vec![];
|
||||
let mut due: Option<i64> = None;
|
||||
let mut rank: Option<f32> = None;
|
||||
|
||||
for val in values {
|
||||
let field: Vec<&str> = val.split(':').collect();
|
||||
if field.len() == 1 {
|
||||
title = field[0].into();
|
||||
continue
|
||||
}
|
||||
|
||||
if field.len() != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
if field[0].starts_with("project") {
|
||||
project.push(field[1].into());
|
||||
}
|
||||
if field[0].starts_with("desc") {
|
||||
desc = Some(field[1].into());
|
||||
}
|
||||
if field[0].starts_with("assign") {
|
||||
assign.push(field[1].into());
|
||||
}
|
||||
if field[0].starts_with("due") {
|
||||
due = due_as_timestamp(&field[1])
|
||||
}
|
||||
if field[0].starts_with("rank") {
|
||||
rank = Some(field[1].parse::<f32>()?);
|
||||
}
|
||||
}
|
||||
|
||||
let task = CliBaseTask { title, desc, project, assign, due, rank };
|
||||
|
||||
Ok(task)
|
||||
}
|
||||
71
bin/tau/tau-cli/src/filter.rs
Normal file
71
bin/tau/tau-cli/src/filter.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use chrono::{Datelike, NaiveDate, NaiveDateTime};
|
||||
use serde_json::Value;
|
||||
|
||||
use super::primitives::{TaskEvent, TaskInfo};
|
||||
|
||||
// Helper function to check task's state
|
||||
fn check_task_state(task: &TaskInfo, state: &str) -> bool {
|
||||
let last_state = task.events.last().unwrap_or(&TaskEvent::default()).action.clone();
|
||||
state == last_state
|
||||
}
|
||||
|
||||
pub fn apply_filter(tasks: &mut Vec<TaskInfo>, filter: String) {
|
||||
match filter.as_str() {
|
||||
"open" => tasks.retain(|task| check_task_state(task, "open")),
|
||||
"pause" => tasks.retain(|task| check_task_state(task, "pause")),
|
||||
|
||||
_ if filter.len() == 4 && filter.parse::<u32>().is_ok() => {
|
||||
let (month, year) =
|
||||
(filter[..2].parse::<u32>().unwrap(), filter[2..].parse::<i32>().unwrap());
|
||||
|
||||
let year = year + 2000;
|
||||
|
||||
tasks.retain(|task| {
|
||||
let date = task.created_at;
|
||||
let task_date = NaiveDateTime::from_timestamp(date, 0).date();
|
||||
let filter_date = NaiveDate::from_ymd(year, month, 1);
|
||||
task_date.month() == filter_date.month() && task_date.year() == filter_date.year()
|
||||
})
|
||||
}
|
||||
|
||||
_ if filter.contains("assign:") => {
|
||||
let kv: Vec<&str> = filter.split(':').collect();
|
||||
if kv.len() == 2 {
|
||||
let value = Value::from(kv[1]);
|
||||
if value.as_str().is_some() {
|
||||
tasks.retain(|task| task.assign.contains(&value.as_str().unwrap().into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ if filter.contains("project:") => {
|
||||
let kv: Vec<&str> = filter.split(':').collect();
|
||||
if kv.len() == 2 {
|
||||
let value = Value::from(kv[1]);
|
||||
if value.as_str().is_some() {
|
||||
tasks.retain(|task| task.project.contains(&value.as_str().unwrap().into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ if filter.contains("rank:") => {
|
||||
let kv: Vec<&str> = filter.split(':').collect();
|
||||
|
||||
if kv.len() == 3 {
|
||||
let value = kv[2].parse::<f32>().unwrap_or(0.0);
|
||||
|
||||
tasks.retain(|task| {
|
||||
if filter.contains("lt") {
|
||||
task.rank < value
|
||||
} else if filter.contains("gt") {
|
||||
task.rank > value
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
@@ -75,14 +75,6 @@ pub async fn set_state(url: &str, id: u64, state: &str) -> Result<Value> {
|
||||
request(req, url.to_string()).await
|
||||
}
|
||||
|
||||
// Get task's state.
|
||||
// --> {"jsonrpc": "2.0", "method": "get_state", "params": [task_id], "id": 1}
|
||||
// <-- {"jsonrpc": "2.0", "result": "state", "id": 1}
|
||||
pub async fn get_state(url: &str, id: u64) -> Result<Value> {
|
||||
let req = jsonrpc::request(json!("get_state"), json!([id]));
|
||||
request(req, url.to_string()).await
|
||||
}
|
||||
|
||||
// Set comment for a task and returns `true` upon success.
|
||||
// --> {"jsonrpc": "2.0", "method": "set_comment", "params": [task_id, comment_content], "id": 1}
|
||||
// <-- {"jsonrpc": "2.0", "result": true, "id": 1}
|
||||
@@ -92,9 +84,9 @@ pub async fn set_comment(url: &str, id: u64, content: &str) -> Result<Value> {
|
||||
}
|
||||
|
||||
// Get task by id.
|
||||
// --> {"jsonrpc": "2.0", "method": "get_by_id", "params": [task_id], "id": 1}
|
||||
// --> {"jsonrpc": "2.0", "method": "get_task_by_id", "params": [task_id], "id": 1}
|
||||
// <-- {"jsonrpc": "2.0", "result": "task", "id": 1}
|
||||
pub async fn get_by_id(url: &str, id: u64) -> Result<Value> {
|
||||
let req = jsonrpc::request(json!("get_by_id"), json!([id]));
|
||||
pub async fn get_task_by_id(url: &str, id: u64) -> Result<Value> {
|
||||
let req = jsonrpc::request(json!("get_task_by_id"), json!([id]));
|
||||
request(req, url.to_string()).await
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use log::error;
|
||||
use serde_json::{json, Value};
|
||||
use serde_json::json;
|
||||
use simplelog::{ColorChoice, TermLogger, TerminalMode};
|
||||
use structopt_toml::StructOptToml;
|
||||
|
||||
use darkfi::{
|
||||
util::{
|
||||
@@ -9,29 +10,29 @@ use darkfi::{
|
||||
},
|
||||
Result,
|
||||
};
|
||||
use structopt_toml::StructOptToml;
|
||||
|
||||
mod cli;
|
||||
mod filter;
|
||||
mod jsonrpc;
|
||||
mod primitives;
|
||||
mod util;
|
||||
mod view;
|
||||
|
||||
use crate::{
|
||||
jsonrpc::{add, get_by_id, get_state, list, set_comment, set_state, update},
|
||||
util::{
|
||||
desc_in_editor, due_as_timestamp, get_comments, list_tasks, set_title, show_task, CliTau,
|
||||
CliTauSubCommands, TaskInfo, CONFIG_FILE, CONFIG_FILE_CONTENTS,
|
||||
},
|
||||
};
|
||||
use cli::CliTauSubCommands;
|
||||
use primitives::TaskInfo;
|
||||
use util::{desc_in_editor, CONFIG_FILE, CONFIG_FILE_CONTENTS};
|
||||
use view::{comments_as_string, events_as_string, print_list_of_task, print_task_info};
|
||||
|
||||
async fn start(mut options: CliTau) -> Result<()> {
|
||||
async fn start(mut options: cli::CliTau) -> Result<()> {
|
||||
let rpc_addr = &format!("tcp://{}", &options.rpc_listen.clone());
|
||||
|
||||
let states: Vec<String> = vec!["stop".into(), "open".into(), "pause".into()];
|
||||
|
||||
match options.id {
|
||||
Some(id) if id.len() < 4 && id.parse::<u64>().is_ok() => {
|
||||
let task = get_by_id(rpc_addr, id.parse::<u64>().unwrap()).await?;
|
||||
let task = jsonrpc::get_task_by_id(rpc_addr, id.parse::<u64>().unwrap()).await?;
|
||||
let taskinfo: TaskInfo = serde_json::from_value(task.clone())?;
|
||||
let current_state: String =
|
||||
serde_json::from_value(get_state(rpc_addr, id.parse::<u64>().unwrap()).await?)?;
|
||||
show_task(task, taskinfo, current_state)?;
|
||||
print_task_info(taskinfo)?;
|
||||
return Ok(())
|
||||
}
|
||||
Some(id) => options.filters.push(id),
|
||||
@@ -39,93 +40,58 @@ async fn start(mut options: CliTau) -> Result<()> {
|
||||
}
|
||||
|
||||
match options.command {
|
||||
Some(CliTauSubCommands::Add { title, desc, assign, project, due, rank }) => {
|
||||
let title = match title {
|
||||
Some(t) => t,
|
||||
None => set_title()?,
|
||||
Some(CliTauSubCommands::Add { values }) => {
|
||||
let mut task = cli::task_from_cli_values(values)?;
|
||||
if task.title.is_empty() {
|
||||
error!("Provide a title for the task");
|
||||
return Ok(())
|
||||
};
|
||||
|
||||
let desc = match desc {
|
||||
Some(d) => Some(d),
|
||||
None => desc_in_editor()?,
|
||||
if task.desc.is_none() {
|
||||
task.desc = desc_in_editor()?;
|
||||
};
|
||||
|
||||
let assign: Vec<String> = match assign {
|
||||
Some(a) => a.split(',').map(|s| s.into()).collect(),
|
||||
None => vec![],
|
||||
};
|
||||
|
||||
let project: Vec<String> = match project {
|
||||
Some(p) => p.split(',').map(|s| s.into()).collect(),
|
||||
None => vec![],
|
||||
};
|
||||
|
||||
let due = match due {
|
||||
Some(d) => due_as_timestamp(&d),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let rank = rank.unwrap_or(0.0);
|
||||
|
||||
add(rpc_addr,
|
||||
json!([{"title": title, "desc": desc, "assign": assign, "project": project, "due": due, "rank": rank}]),
|
||||
).await?;
|
||||
jsonrpc::add(rpc_addr, json!([task])).await?;
|
||||
}
|
||||
|
||||
Some(CliTauSubCommands::Update { id, key, value }) => {
|
||||
let value = value.as_str().trim();
|
||||
|
||||
let updated_value: Value = match key.as_str() {
|
||||
"due" => {
|
||||
json!(due_as_timestamp(value))
|
||||
}
|
||||
"rank" => {
|
||||
json!(value.parse::<f32>()?)
|
||||
}
|
||||
"project" | "assign" => {
|
||||
json!(value.split(',').collect::<Vec<&str>>())
|
||||
}
|
||||
_ => {
|
||||
json!(value)
|
||||
}
|
||||
};
|
||||
|
||||
update(rpc_addr, id, json!({ key: updated_value })).await?;
|
||||
Some(CliTauSubCommands::Update { id, values }) => {
|
||||
let task = cli::task_from_cli_values(values)?;
|
||||
jsonrpc::update(rpc_addr, id, json!([task])).await?;
|
||||
}
|
||||
|
||||
Some(CliTauSubCommands::State { id, state }) => match state {
|
||||
Some(state) => {
|
||||
if state.as_str() == "open" {
|
||||
set_state(rpc_addr, id, state.trim()).await?;
|
||||
} else if state.as_str() == "pause" {
|
||||
set_state(rpc_addr, id, state.trim()).await?;
|
||||
} else if state.as_str() == "stop" {
|
||||
set_state(rpc_addr, id, state.trim()).await?;
|
||||
let state = state.trim().to_lowercase();
|
||||
if states.contains(&state) {
|
||||
jsonrpc::set_state(rpc_addr, id, &state).await?;
|
||||
} else {
|
||||
error!("Task state could only be one of three states: open, pause or stop");
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let state = get_state(rpc_addr, id).await?;
|
||||
println!("Task with id {} is: {}", id, state);
|
||||
let task = jsonrpc::get_task_by_id(rpc_addr, id).await?;
|
||||
let taskinfo: TaskInfo = serde_json::from_value(task.clone())?;
|
||||
let state = events_as_string(taskinfo.events);
|
||||
println!("Task {}: {}", id, state);
|
||||
}
|
||||
},
|
||||
|
||||
Some(CliTauSubCommands::Comment { id, content }) => match content {
|
||||
Some(content) => {
|
||||
set_comment(rpc_addr, id, content.trim()).await?;
|
||||
jsonrpc::set_comment(rpc_addr, id, content.trim()).await?;
|
||||
}
|
||||
None => {
|
||||
let rep = get_by_id(rpc_addr, id).await?;
|
||||
let comments = get_comments(rep)?;
|
||||
|
||||
println!("Comments on Task with id {}:\n{}", id, comments);
|
||||
let task = jsonrpc::get_task_by_id(rpc_addr, id).await?;
|
||||
let taskinfo: TaskInfo = serde_json::from_value(task.clone())?;
|
||||
let comments = comments_as_string(taskinfo.comments);
|
||||
println!("Comments {}:\n{}", id, comments);
|
||||
}
|
||||
},
|
||||
|
||||
Some(CliTauSubCommands::List {}) | None => {
|
||||
let rep = list(rpc_addr, json!([])).await?;
|
||||
list_tasks(rep, options.filters)?;
|
||||
let tasks = jsonrpc::list(rpc_addr, json!([])).await?;
|
||||
let mut tasks: Vec<TaskInfo> = serde_json::from_value(tasks)?;
|
||||
print_list_of_task(&mut tasks, options.filters)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,10 +100,10 @@ async fn start(mut options: CliTau) -> Result<()> {
|
||||
|
||||
#[async_std::main]
|
||||
async fn main() -> Result<()> {
|
||||
let args = CliTau::from_args_with_toml("").unwrap();
|
||||
let args = cli::CliTau::from_args_with_toml("").unwrap();
|
||||
let cfg_path = get_config_path(args.config, CONFIG_FILE)?;
|
||||
spawn_config(&cfg_path, CONFIG_FILE_CONTENTS.as_bytes())?;
|
||||
let args = CliTau::from_args_with_toml(&std::fs::read_to_string(cfg_path)?).unwrap();
|
||||
let args = cli::CliTau::from_args_with_toml(&std::fs::read_to_string(cfg_path)?).unwrap();
|
||||
|
||||
let (lvl, conf) = log_config(args.verbose.into())?;
|
||||
TermLogger::init(lvl, conf, TerminalMode::Mixed, ColorChoice::Auto)?;
|
||||
|
||||
63
bin/tau/tau-cli/src/primitives.rs
Normal file
63
bin/tau/tau-cli/src/primitives.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::util::timestamp_to_date;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct TaskInfo {
|
||||
pub ref_id: String,
|
||||
pub id: u32,
|
||||
pub title: String,
|
||||
pub desc: String,
|
||||
pub owner: String,
|
||||
pub assign: Vec<String>,
|
||||
pub project: Vec<String>,
|
||||
pub due: Option<i64>,
|
||||
pub rank: f32,
|
||||
pub created_at: i64,
|
||||
pub events: Vec<TaskEvent>,
|
||||
pub comments: Vec<Comment>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct TaskEvent {
|
||||
pub action: String,
|
||||
pub timestamp: Timestamp,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TaskEvent {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "action: {}, timestamp: {}", self.action, self.timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TaskEvent {
|
||||
fn default() -> Self {
|
||||
TaskEvent {
|
||||
action: "open".to_string(),
|
||||
timestamp: Timestamp(chrono::offset::Local::now().timestamp()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Comment {
|
||||
content: String,
|
||||
author: String,
|
||||
timestamp: Timestamp,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Comment {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "{} author: {}, content: {} ", self.timestamp, self.author, self.content)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Timestamp(pub i64);
|
||||
|
||||
impl std::fmt::Display for Timestamp {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
let date = timestamp_to_date(self.0, "datetime");
|
||||
write!(f, "{}", date)
|
||||
}
|
||||
}
|
||||
@@ -1,109 +1,19 @@
|
||||
use std::{
|
||||
env::{temp_dir, var},
|
||||
fs::{self, File},
|
||||
io::{self, Read, Write},
|
||||
net::SocketAddr,
|
||||
ops::Index,
|
||||
io::Read,
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use chrono::{Datelike, Local, NaiveDate, NaiveDateTime};
|
||||
use log::error;
|
||||
use prettytable::{cell, format, row, table, Cell, Row, Table};
|
||||
use rand::distributions::{Alphanumeric, DistString};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use darkfi::{Error, Result};
|
||||
use structopt::StructOpt;
|
||||
use structopt_toml::StructOptToml;
|
||||
|
||||
pub const CONFIG_FILE: &str = "taud_config.toml";
|
||||
pub const CONFIG_FILE_CONTENTS: &str = include_str!("../../taud_config.toml");
|
||||
|
||||
#[derive(StructOpt, Deserialize, Debug)]
|
||||
pub enum CliTauSubCommands {
|
||||
/// Add a new task
|
||||
Add {
|
||||
/// Specify task title
|
||||
title: Option<String>,
|
||||
/// Specify task description
|
||||
desc: Option<String>,
|
||||
/// Assign task to user
|
||||
assign: Option<String>,
|
||||
/// Task project (can be hierarchical: crypto.zk)
|
||||
project: Option<String>,
|
||||
/// Due date in DDMM format: "2202" for 22 Feb
|
||||
due: Option<String>,
|
||||
/// Project rank single precision decimal real value: 4.8761
|
||||
rank: Option<f32>,
|
||||
},
|
||||
/// Update/Edit an existing task by ID
|
||||
Update {
|
||||
/// Task ID
|
||||
id: u64,
|
||||
/// Field's name (ex title)
|
||||
key: String,
|
||||
/// New value
|
||||
value: String,
|
||||
},
|
||||
/// Set or Get task state
|
||||
State {
|
||||
/// Task ID
|
||||
id: u64,
|
||||
/// Set task state
|
||||
state: Option<String>,
|
||||
},
|
||||
/// Set or Get comment for a task
|
||||
Comment {
|
||||
/// Task ID
|
||||
id: u64,
|
||||
/// Comment content
|
||||
content: Option<String>,
|
||||
},
|
||||
/// List all tasks
|
||||
List {},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct TaskInfo {
|
||||
pub ref_id: String,
|
||||
pub id: u32,
|
||||
pub title: String,
|
||||
pub desc: String,
|
||||
pub owner: String,
|
||||
pub assign: Vec<String>,
|
||||
pub project: Vec<String>,
|
||||
pub due: Option<i64>,
|
||||
pub rank: f32,
|
||||
pub created_at: i64,
|
||||
pub events: Vec<Value>,
|
||||
pub comments: Vec<Value>,
|
||||
}
|
||||
|
||||
/// Tau cli
|
||||
#[derive(Debug, Deserialize, StructOpt, StructOptToml)]
|
||||
#[serde(default)]
|
||||
#[structopt(name = "tau")]
|
||||
pub struct CliTau {
|
||||
/// Increase verbosity
|
||||
#[structopt(short, parse(from_occurrences))]
|
||||
pub verbose: u8,
|
||||
/// JSON-RPC listen URL
|
||||
#[structopt(long = "rpc", default_value = "127.0.0.1:11055")]
|
||||
pub rpc_listen: SocketAddr,
|
||||
/// Sets a custom config file
|
||||
#[structopt(short, long)]
|
||||
pub config: Option<String>,
|
||||
#[structopt(subcommand)]
|
||||
pub command: Option<CliTauSubCommands>,
|
||||
/// Get task by ID
|
||||
pub id: Option<String>,
|
||||
/// Search criteria (zero or more)
|
||||
#[structopt(multiple = true)]
|
||||
pub filters: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn due_as_timestamp(due: &str) -> Option<i64> {
|
||||
if due.len() == 4 {
|
||||
let (day, month) = (due[..2].parse::<u32>().unwrap(), due[2..].parse::<u32>().unwrap());
|
||||
@@ -130,24 +40,6 @@ pub fn due_as_timestamp(due: &str) -> Option<i64> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn set_title() -> Result<String> {
|
||||
print!("Title: ");
|
||||
io::stdout().flush()?;
|
||||
let mut t = String::new();
|
||||
io::stdin().read_line(&mut t)?;
|
||||
|
||||
if t.is_empty() {
|
||||
error!("You can't have a task without a title");
|
||||
return Err(Error::OperationFailed)
|
||||
}
|
||||
|
||||
if &t[(t.len() - 1)..] == "\n" {
|
||||
t.pop();
|
||||
}
|
||||
|
||||
Ok(t)
|
||||
}
|
||||
|
||||
pub fn desc_in_editor() -> Result<Option<String>> {
|
||||
// Create a temporary file with some comments inside
|
||||
let mut file_path = temp_dir();
|
||||
@@ -186,71 +78,7 @@ pub fn desc_in_editor() -> Result<Option<String>> {
|
||||
Ok(Some(description))
|
||||
}
|
||||
|
||||
pub fn show_task(task: Value, taskinfo: TaskInfo, current_state: String) -> Result<()> {
|
||||
let mut table = table!([Bd => "ref_id", &taskinfo.ref_id],
|
||||
["id", &taskinfo.id.to_string()],
|
||||
[Bd =>"owner", &taskinfo.owner],
|
||||
[Bd =>"title", &taskinfo.title],
|
||||
["desc", &taskinfo.desc],
|
||||
[Bd =>"assign", get_from_task(task.clone(), "assign")?],
|
||||
["project", get_from_task(task.clone(), "project")?],
|
||||
[Bd =>"due", timestamp_to_date(task["due"].clone(),"date")],
|
||||
["rank", &taskinfo.rank.to_string()],
|
||||
[Bd =>"created_at", timestamp_to_date(task["created_at"].clone(), "datetime")],
|
||||
["current_state", ¤t_state],
|
||||
[Bd => "comments", get_comments(task.clone())?]);
|
||||
|
||||
table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
|
||||
table.set_titles(row!["Name", "Value"]);
|
||||
|
||||
table.printstd();
|
||||
|
||||
let mut event_table = table!(["events", get_events(task)?]);
|
||||
event_table.set_format(*format::consts::FORMAT_NO_COLSEP);
|
||||
|
||||
event_table.printstd();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_comments(rep: Value) -> Result<String> {
|
||||
let task: Value = serde_json::from_value(rep)?;
|
||||
|
||||
let comments: Vec<Value> = serde_json::from_value(task["comments"].clone())?;
|
||||
let mut result = String::new();
|
||||
|
||||
for comment in comments {
|
||||
result.push_str(comment["author"].as_str().ok_or(Error::OperationFailed)?);
|
||||
result.push_str(": ");
|
||||
result.push_str(comment["content"].as_str().ok_or(Error::OperationFailed)?);
|
||||
result.push('\n');
|
||||
}
|
||||
result.pop();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn get_events(rep: Value) -> Result<String> {
|
||||
let task: Value = serde_json::from_value(rep)?;
|
||||
|
||||
let events: Vec<Value> = serde_json::from_value(task["events"].clone())?;
|
||||
let mut ev = String::new();
|
||||
|
||||
for event in events {
|
||||
ev.push_str("State changed to ");
|
||||
ev.push_str(event["action"].as_str().ok_or(Error::OperationFailed)?);
|
||||
ev.push_str(" at ");
|
||||
ev.push_str(×tamp_to_date(event["timestamp"].clone(), "datetime"));
|
||||
ev.push('\n');
|
||||
}
|
||||
ev.pop();
|
||||
|
||||
Ok(ev)
|
||||
}
|
||||
|
||||
pub fn timestamp_to_date(timestamp: Value, dt: &str) -> String {
|
||||
let timestamp = timestamp.as_i64().unwrap_or(0);
|
||||
|
||||
pub fn timestamp_to_date(timestamp: i64, dt: &str) -> String {
|
||||
if timestamp <= 0 {
|
||||
return "".to_string()
|
||||
}
|
||||
@@ -265,139 +93,3 @@ pub fn timestamp_to_date(timestamp: Value, dt: &str) -> String {
|
||||
_ => "".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_from_task(task: Value, value: &str) -> Result<String> {
|
||||
let vec_values: Vec<Value> = serde_json::from_value(task[value].clone())?;
|
||||
let mut result = String::new();
|
||||
for (i, _) in vec_values.iter().enumerate() {
|
||||
if !result.is_empty() {
|
||||
result.push(',');
|
||||
}
|
||||
result.push_str(vec_values.index(i).as_str().unwrap());
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
// Helper function to check task's state
|
||||
fn check_task_state(task: &Value, state: &str) -> bool {
|
||||
let events = match task["events"].as_array() {
|
||||
Some(t) => t.to_owned(),
|
||||
None => {
|
||||
error!("Value is not an array!");
|
||||
vec![]
|
||||
}
|
||||
};
|
||||
|
||||
let last_state = match events.last() {
|
||||
Some(s) => s["action"].as_str().unwrap(),
|
||||
None => "open",
|
||||
};
|
||||
state == last_state
|
||||
}
|
||||
|
||||
fn apply_filter(tasks: &mut Vec<Value>, filter: String) {
|
||||
match filter.as_str() {
|
||||
"open" => tasks.retain(|task| check_task_state(task, "open")),
|
||||
"pause" => tasks.retain(|task| check_task_state(task, "pause")),
|
||||
|
||||
_ if filter.len() == 4 && filter.parse::<u32>().is_ok() => {
|
||||
let (month, year) =
|
||||
(filter[..2].parse::<u32>().unwrap(), filter[2..].parse::<i32>().unwrap());
|
||||
|
||||
let year = year + 2000;
|
||||
|
||||
tasks.retain(|task| {
|
||||
let date = task["created_at"].as_i64().unwrap();
|
||||
let task_date = NaiveDateTime::from_timestamp(date, 0).date();
|
||||
let filter_date = NaiveDate::from_ymd(year, month, 1);
|
||||
task_date.month() == filter_date.month() && task_date.year() == filter_date.year()
|
||||
})
|
||||
}
|
||||
|
||||
_ if filter.contains("assign:") | filter.contains("project:") => {
|
||||
let kv: Vec<&str> = filter.split(':').collect();
|
||||
let key = kv[0];
|
||||
let value = Value::from(kv[1]);
|
||||
|
||||
tasks.retain(|task| task[key].as_array().unwrap_or(&vec![]).contains(&value))
|
||||
}
|
||||
|
||||
_ if filter.contains("rank>") | filter.contains("rank<") => {
|
||||
let kv: Vec<&str> = if filter.contains('>') {
|
||||
filter.split('>').collect()
|
||||
} else {
|
||||
filter.split('<').collect()
|
||||
};
|
||||
let key = kv[0];
|
||||
let value = kv[1].parse::<f32>().unwrap_or(0.0);
|
||||
|
||||
tasks.retain(|task| {
|
||||
let rank = task[key].as_f64().unwrap_or(0.0) as f32;
|
||||
if filter.contains('>') {
|
||||
rank > value
|
||||
} else {
|
||||
rank < value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn list_tasks(rep: Value, filters: Vec<String>) -> Result<()> {
|
||||
let mut table = Table::new();
|
||||
table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
|
||||
table.set_titles(row!["ID", "Title", "Project", "Assigned", "Due", "Rank"]);
|
||||
|
||||
let mut tasks: Vec<Value> = serde_json::from_value(rep)?;
|
||||
|
||||
for filter in filters {
|
||||
apply_filter(&mut tasks, filter);
|
||||
}
|
||||
|
||||
tasks.sort_by(|a, b| b["rank"].as_f64().partial_cmp(&a["rank"].as_f64()).unwrap());
|
||||
|
||||
let (max_rank, min_rank) = if !tasks.is_empty() {
|
||||
(
|
||||
serde_json::from_value(tasks[0]["rank"].clone())?,
|
||||
serde_json::from_value(tasks[tasks.len() - 1]["rank"].clone())?,
|
||||
)
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
};
|
||||
|
||||
for task in tasks {
|
||||
let events: Vec<Value> = serde_json::from_value(task["events"].clone())?;
|
||||
let state = match events.last() {
|
||||
Some(s) => s["action"].as_str().unwrap(),
|
||||
None => "open",
|
||||
};
|
||||
|
||||
let rank = task["rank"].as_f64().unwrap_or(0.0) as f32;
|
||||
|
||||
let (max_style, min_style, mid_style, gen_style) = if state == "open" {
|
||||
("bFC", "Fb", "Fc", "")
|
||||
} else {
|
||||
("iFYBd", "iFYBd", "iFYBd", "iFYBd")
|
||||
};
|
||||
|
||||
table.add_row(Row::new(vec![
|
||||
Cell::new(&task["id"].to_string()).style_spec(gen_style),
|
||||
Cell::new(task["title"].as_str().unwrap()).style_spec(gen_style),
|
||||
Cell::new(&get_from_task(task.clone(), "project")?).style_spec(gen_style),
|
||||
Cell::new(&get_from_task(task.clone(), "assign")?).style_spec(gen_style),
|
||||
Cell::new(×tamp_to_date(task["due"].clone(), "date")).style_spec(gen_style),
|
||||
if rank == max_rank {
|
||||
Cell::new(&rank.to_string()).style_spec(max_style)
|
||||
} else if rank == min_rank {
|
||||
Cell::new(&rank.to_string()).style_spec(min_style)
|
||||
} else {
|
||||
Cell::new(&rank.to_string()).style_spec(mid_style)
|
||||
},
|
||||
]));
|
||||
}
|
||||
table.printstd();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
125
bin/tau/tau-cli/src/view.rs
Normal file
125
bin/tau/tau-cli/src/view.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
use prettytable::{cell, format, row, table, Cell, Row, Table};
|
||||
|
||||
use darkfi::Result;
|
||||
|
||||
use super::{
|
||||
filter::apply_filter,
|
||||
primitives::{Comment, TaskEvent, TaskInfo},
|
||||
util::timestamp_to_date,
|
||||
};
|
||||
|
||||
pub fn print_list_of_task(tasks: &mut Vec<TaskInfo>, filters: Vec<String>) -> Result<()> {
|
||||
let mut table = Table::new();
|
||||
|
||||
table.set_format(
|
||||
format::FormatBuilder::new()
|
||||
.padding(1, 1)
|
||||
.separators(
|
||||
&[format::LinePosition::Title],
|
||||
format::LineSeparator::new('─', ' ', ' ', ' '),
|
||||
)
|
||||
.build(),
|
||||
);
|
||||
|
||||
table.set_titles(row!["ID", "Title", "Project", "Assigned", "Due", "Rank"]);
|
||||
|
||||
for filter in filters {
|
||||
apply_filter(tasks, filter);
|
||||
}
|
||||
|
||||
tasks.sort_by(|a, b| b.rank.partial_cmp(&a.rank).unwrap());
|
||||
|
||||
let mut min_rank = 0.0;
|
||||
let mut max_rank = 0.0;
|
||||
if tasks.first().is_some() {
|
||||
max_rank = tasks.first().unwrap().rank;
|
||||
}
|
||||
if tasks.last().is_some() {
|
||||
min_rank = tasks.last().unwrap().rank;
|
||||
}
|
||||
|
||||
for task in tasks {
|
||||
let state = task.events.last().unwrap_or(&TaskEvent::default()).action.clone();
|
||||
|
||||
let (max_style, min_style, mid_style, gen_style) = if state == "open" {
|
||||
("bFC", "Fb", "Fc", "")
|
||||
} else {
|
||||
("iFYBd", "iFYBd", "iFYBd", "iFYBd")
|
||||
};
|
||||
|
||||
let rank = task.rank.to_string();
|
||||
|
||||
table.add_row(Row::new(vec![
|
||||
Cell::new(&task.id.to_string()).style_spec(gen_style),
|
||||
Cell::new(&task.title).style_spec(gen_style),
|
||||
Cell::new(&task.project.join(", ")).style_spec(gen_style),
|
||||
Cell::new(&task.assign.join(", ")).style_spec(gen_style),
|
||||
Cell::new(×tamp_to_date(task.due.unwrap_or(0), "date")).style_spec(gen_style),
|
||||
if task.rank == max_rank {
|
||||
Cell::new(&rank).style_spec(max_style)
|
||||
} else if task.rank == min_rank {
|
||||
Cell::new(&rank).style_spec(min_style)
|
||||
} else {
|
||||
Cell::new(&rank).style_spec(mid_style)
|
||||
},
|
||||
]));
|
||||
}
|
||||
table.printstd();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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), "date");
|
||||
let created_at = timestamp_to_date(taskinfo.created_at, "datetime");
|
||||
let mut table = table!([Bd => "ref_id", &taskinfo.ref_id],
|
||||
["id", &taskinfo.id.to_string()],
|
||||
[Bd =>"owner", &taskinfo.owner],
|
||||
[Bd =>"title", &taskinfo.title],
|
||||
["desc", &taskinfo.desc.to_string()],
|
||||
[Bd =>"assign", taskinfo.assign.join(", ")],
|
||||
["project", taskinfo.project.join(", ")],
|
||||
[Bd =>"due", due],
|
||||
["rank", &taskinfo.rank.to_string()],
|
||||
[Bd =>"created_at", created_at],
|
||||
["current_state", current_state]);
|
||||
|
||||
table.set_format(
|
||||
format::FormatBuilder::new()
|
||||
.padding(1, 1)
|
||||
.separators(
|
||||
&[format::LinePosition::Title],
|
||||
format::LineSeparator::new('─', ' ', ' ', ' '),
|
||||
)
|
||||
.build(),
|
||||
);
|
||||
table.set_titles(row!["Name", "Value"]);
|
||||
|
||||
table.printstd();
|
||||
|
||||
let mut event_table = table!(["events", &events_as_string(taskinfo.events)]);
|
||||
event_table.set_format(*format::consts::FORMAT_NO_COLSEP);
|
||||
|
||||
event_table.printstd();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn comments_as_string(comments: Vec<Comment>) -> String {
|
||||
let mut comments_str = String::new();
|
||||
for comment in comments {
|
||||
comments_str.push_str(&comment.to_string());
|
||||
comments_str.push('\n');
|
||||
}
|
||||
comments_str
|
||||
}
|
||||
|
||||
pub fn events_as_string(events: Vec<TaskEvent>) -> String {
|
||||
let mut events_str = String::new();
|
||||
for event in events {
|
||||
events_str.push_str(&event.to_string());
|
||||
events_str.push('\n');
|
||||
}
|
||||
events_str
|
||||
}
|
||||
@@ -34,7 +34,7 @@ struct BaseTaskInfo {
|
||||
assign: Vec<String>,
|
||||
project: Vec<String>,
|
||||
due: Option<Timestamp>,
|
||||
rank: f32,
|
||||
rank: Option<f32>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -54,10 +54,9 @@ impl RequestHandler for JsonRpcInterface {
|
||||
Some("add") => self.add(req.params).await,
|
||||
Some("list") => self.list(req.params).await,
|
||||
Some("update") => self.update(req.params).await,
|
||||
Some("get_state") => self.get_state(req.params).await,
|
||||
Some("set_state") => self.set_state(req.params).await,
|
||||
Some("set_comment") => self.set_comment(req.params).await,
|
||||
Some("get_by_id") => self.get_by_id(req.params).await,
|
||||
Some("get_task_by_id") => self.get_task_by_id(req.params).await,
|
||||
Some(_) | None => {
|
||||
return JsonResult::Err(jsonerr(ErrorCode::MethodNotFound, None, req.id))
|
||||
}
|
||||
@@ -101,7 +100,7 @@ impl JsonRpcInterface {
|
||||
&task.desc,
|
||||
&self.nickname,
|
||||
task.due,
|
||||
task.rank,
|
||||
task.rank.unwrap_or(0.0),
|
||||
&self.dataset_path,
|
||||
)?;
|
||||
new_task.set_project(&task.project);
|
||||
@@ -141,23 +140,6 @@ impl JsonRpcInterface {
|
||||
Ok(json!(true))
|
||||
}
|
||||
|
||||
// RPCAPI:
|
||||
// Get task's state.
|
||||
// --> {"jsonrpc": "2.0", "method": "get_state", "params": [task_id], "id": 1}
|
||||
// <-- {"jsonrpc": "2.0", "result": "state", "id": 1}
|
||||
async fn get_state(&self, params: Value) -> TaudResult<Value> {
|
||||
debug!(target: "tau", "JsonRpc::get_state() params {}", params);
|
||||
let args = params.as_array().unwrap();
|
||||
|
||||
if args.len() != 1 {
|
||||
return Err(TaudError::InvalidData("len of params should be 1".into()))
|
||||
}
|
||||
|
||||
let task: TaskInfo = self.load_task_by_id(&args[0])?;
|
||||
|
||||
Ok(json!(task.get_state()))
|
||||
}
|
||||
|
||||
// RPCAPI:
|
||||
// Set state for a task and returns `true` upon success.
|
||||
// --> {"jsonrpc": "2.0", "method": "set_state", "params": [task_id, state], "id": 1}
|
||||
@@ -203,10 +185,10 @@ impl JsonRpcInterface {
|
||||
|
||||
// RPCAPI:
|
||||
// Get a task by id.
|
||||
// --> {"jsonrpc": "2.0", "method": "get_by_id", "params": [task_id], "id": 1}
|
||||
// --> {"jsonrpc": "2.0", "method": "get_task_by_id", "params": [task_id], "id": 1}
|
||||
// <-- {"jsonrpc": "2.0", "result": "task", "id": 1}
|
||||
async fn get_by_id(&self, params: Value) -> TaudResult<Value> {
|
||||
debug!(target: "tau", "JsonRpc::get_by_id() params {}", params);
|
||||
async fn get_task_by_id(&self, params: Value) -> TaudResult<Value> {
|
||||
debug!(target: "tau", "JsonRpc::get_task_by_id() params {}", params);
|
||||
let args = params.as_array().unwrap();
|
||||
|
||||
if args.len() != 1 {
|
||||
@@ -227,51 +209,72 @@ impl JsonRpcInterface {
|
||||
task.ok_or(TaudError::InvalidId)
|
||||
}
|
||||
|
||||
fn check_params_for_update(&self, task_id: &Value, data: &Value) -> TaudResult<TaskInfo> {
|
||||
fn check_params_for_update(&self, task_id: &Value, fields: &Value) -> TaudResult<TaskInfo> {
|
||||
let mut task: TaskInfo = self.load_task_by_id(task_id)?;
|
||||
|
||||
if !data.is_object() {
|
||||
if !fields.is_array() {
|
||||
return Err(TaudError::InvalidData("Invalid task's data".into()))
|
||||
}
|
||||
|
||||
let data = data.as_object().unwrap();
|
||||
let fields = fields.as_array().unwrap();
|
||||
|
||||
if data.contains_key("title") {
|
||||
let title = data.get("title").unwrap().clone();
|
||||
let title: String = serde_json::from_value(title)?;
|
||||
task.set_title(&title);
|
||||
for field in fields {
|
||||
if !field.is_object() {
|
||||
return Err(TaudError::InvalidData("Invalid task's fields".into()))
|
||||
}
|
||||
|
||||
let field = field.as_object().unwrap();
|
||||
|
||||
if field.contains_key("title") {
|
||||
let title = field.get("title").unwrap().clone();
|
||||
let title: String = serde_json::from_value(title)?;
|
||||
if !title.is_empty() {
|
||||
task.set_title(&title);
|
||||
}
|
||||
}
|
||||
|
||||
if field.contains_key("desc") {
|
||||
let description = field.get("description");
|
||||
if let Some(description) = description {
|
||||
let description: String = serde_json::from_value(description.clone())?;
|
||||
task.set_desc(&description);
|
||||
}
|
||||
}
|
||||
|
||||
if field.contains_key("rank") {
|
||||
let rank_opt = field.get("rank");
|
||||
if let Some(rank) = rank_opt {
|
||||
let rank: Option<f32> = serde_json::from_value(rank.clone())?;
|
||||
if rank.is_some() {
|
||||
task.set_rank(rank.unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if field.contains_key("due") {
|
||||
let due = field.get("due").unwrap().clone();
|
||||
let due: Option<Option<Timestamp>> = serde_json::from_value(due)?;
|
||||
if let Some(d) = due {
|
||||
task.set_due(d);
|
||||
}
|
||||
}
|
||||
|
||||
if field.contains_key("assign") {
|
||||
let assign = field.get("assign").unwrap().clone();
|
||||
let assign: Vec<String> = serde_json::from_value(assign)?;
|
||||
if !assign.is_empty() {
|
||||
task.set_assign(&assign);
|
||||
}
|
||||
}
|
||||
|
||||
if field.contains_key("project") {
|
||||
let project = field.get("project").unwrap().clone();
|
||||
let project: Vec<String> = serde_json::from_value(project)?;
|
||||
if !project.is_empty() {
|
||||
task.set_project(&project);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if data.contains_key("description") {
|
||||
let description = data.get("description").unwrap().clone();
|
||||
let description: String = serde_json::from_value(description)?;
|
||||
task.set_desc(&description);
|
||||
}
|
||||
|
||||
if data.contains_key("rank") {
|
||||
let rank = data.get("rank").unwrap().clone();
|
||||
let rank: f32 = serde_json::from_value(rank)?;
|
||||
task.set_rank(rank);
|
||||
}
|
||||
|
||||
if data.contains_key("due") {
|
||||
let due = data.get("due").unwrap().clone();
|
||||
let due = serde_json::from_value(due)?;
|
||||
task.set_due(Some(due));
|
||||
}
|
||||
|
||||
if data.contains_key("assign") {
|
||||
let assign = data.get("assign").unwrap().clone();
|
||||
let assign: Vec<String> = serde_json::from_value(assign)?;
|
||||
task.set_assign(&assign);
|
||||
}
|
||||
|
||||
if data.contains_key("project") {
|
||||
let project = data.get("project").unwrap().clone();
|
||||
let project: Vec<String> = serde_json::from_value(project)?;
|
||||
task.set_project(&project);
|
||||
}
|
||||
|
||||
Ok(task)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user