Skip to content

Commit

Permalink
add support for multiple groups
Browse files Browse the repository at this point in the history
  • Loading branch information
Ninjani committed Dec 31, 2023
1 parent 6771359 commit aa62233
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 93 deletions.
191 changes: 122 additions & 69 deletions src/configuration.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use std::collections::{HashMap, HashSet};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::{env, fmt, fs, io};

use chrono::Utc;
use color_eyre::Help;
use dialoguer::{theme, Confirm, Input, Select};
use dialoguer::{theme, Confirm, Input, MultiSelect, Select};
use directories_next::{ProjectDirs, UserDirs};
use eyre::eyre;
use hypothesis::annotations::{Annotation, Document, Permissions, Selector, Target, UserInfo};
Expand All @@ -21,6 +22,7 @@ pub static DEFAULT_NESTED_TAG: &str = "/";
pub static DEFAULT_ANNOTATION_TEMPLATE: &str = r#"
### {{id}}
Group: {{group_id}} ({{group_name}})
Created: {{date_format "%c" created}}
Tags: {{#each tags}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}
Expand Down Expand Up @@ -51,6 +53,8 @@ pub enum OrderBy {
Empty,
Created,
Updated,
GroupID,
Group,
}

impl fmt::Display for OrderBy {
Expand All @@ -64,6 +68,8 @@ impl fmt::Display for OrderBy {
OrderBy::Empty => write!(f, "empty"),
OrderBy::Created => write!(f, "created"),
OrderBy::Updated => write!(f, "updated"),
OrderBy::GroupID => write!(f, "group_id"),
OrderBy::Group => write!(f, "group"),
}
}
}
Expand All @@ -77,7 +83,6 @@ pub struct GooseberryConfig {
pub(crate) hypothesis_key: Option<String>,
/// Hypothesis group with knowledge base annotations
pub(crate) hypothesis_group: Option<String>,

/// Related to tagging and editing
/// Directory to store `sled` database files
pub(crate) db_dir: PathBuf,
Expand All @@ -103,6 +108,9 @@ pub struct GooseberryConfig {
pub(crate) ignore_tags: Option<Vec<String>>,
/// Define nested tag pattern
pub(crate) nested_tag: Option<String>,
/// Hypothesis groups with knowledge base annotations
#[serde(default)]
pub(crate) hypothesis_groups: HashMap<String, String>,
}

/// Main project directory, cross-platform
Expand All @@ -116,6 +124,7 @@ impl Default for GooseberryConfig {
hypothesis_username: None,
hypothesis_key: None,
hypothesis_group: None,
hypothesis_groups: HashMap::new(),
db_dir: get_project_dir()
.map(|dir| dir.data_dir().join("gooseberry_db"))
.expect("Couldn't make database directory"),
Expand Down Expand Up @@ -146,7 +155,6 @@ impl GooseberryConfig {
r#"
hypothesis_username = '<Hypothesis username>'
hypothesis_key = '<Hypothesis personal API key>'
hypothesis_group = '<Hypothesis group ID to take annotations from>'
db_dir = '<full path to database folder>'
kb_dir = '<knowledge-base folder>'
hierarchy = ['Tag']
Expand Down Expand Up @@ -275,7 +283,7 @@ file_extension = '{}'
}
}
None => {
Ok(confy::load(NAME, None).suggestion(Apologize::ConfigError {
Ok(confy::load(NAME).suggestion(Apologize::ConfigError {
message: "Couldn't load from the default config location, maybe you don't have access? \
Try running `gooseberry config default config_file.toml`, modify the generated file, \
then `export GOOSEBERRY_CONFIG=<full/path/to/config_file.toml>`".into()
Expand All @@ -299,9 +307,12 @@ file_extension = '{}'
{
config.set_credentials().await?;
}

if config.hypothesis_group.is_none() {
config.set_group(None).await?;
if config.hypothesis_groups.is_empty() {
let mut group_ids = Vec::new();
if let Some(ref group_id) = config.hypothesis_group {
group_ids.push(group_id.to_owned());
}
config.set_groups(group_ids).await?;
}
Ok(config)
}
Expand Down Expand Up @@ -407,6 +418,7 @@ file_extension = '{}'
OrderBy::BaseURI,
OrderBy::Title,
OrderBy::ID,
OrderBy::Group,
];
let order = Self::get_order_bys(selections)?;
if order.is_empty() {
Expand Down Expand Up @@ -448,6 +460,7 @@ file_extension = '{}'
OrderBy::Title,
OrderBy::Created,
OrderBy::Updated,
OrderBy::Group,
];
let order = Self::get_order_bys(selections)?;

Expand Down Expand Up @@ -563,7 +576,10 @@ file_extension = '{}'
display_name: Some("test_display_name".to_string()),
}),
};
let test_markdown_annotation = AnnotationTemplate::from_annotation(test_annotation);
let mut group_name_mapping = HashMap::new();
group_name_mapping.insert("group_id".to_owned(), "group_name".to_owned());
let test_markdown_annotation =
AnnotationTemplate::from_annotation(test_annotation, &group_name_mapping);
self.annotation_template = loop {
let template = utils::external_editor_input(
Some(
Expand Down Expand Up @@ -656,7 +672,11 @@ file_extension = '{}'
};
let mut test_annotation_2 = test_annotation_1.clone();
test_annotation_2.text = "Another annotation".to_string();
test_annotation_2.group = "group_id_2".to_string();

let mut group_name_mapping = HashMap::new();
group_name_mapping.insert("group_id".to_owned(), "group_name".to_owned());
group_name_mapping.insert("group_id_2".to_owned(), "group_name_2".to_owned());
let templates = Templates {
annotation_template: self
.annotation_template
Expand All @@ -674,11 +694,16 @@ file_extension = '{}'
},
annotations: vec![test_annotation_1.clone(), test_annotation_2.clone()]
.into_iter()
.map(|a| hbs.render("annotation", &AnnotationTemplate::from_annotation(a)))
.map(|a| {
hbs.render(
"annotation",
&AnnotationTemplate::from_annotation(a, &group_name_mapping),
)
})
.collect::<Result<Vec<String>, _>>()?,
raw_annotations: vec![
AnnotationTemplate::from_annotation(test_annotation_1),
AnnotationTemplate::from_annotation(test_annotation_2),
AnnotationTemplate::from_annotation(test_annotation_1, &group_name_mapping),
AnnotationTemplate::from_annotation(test_annotation_2, &group_name_mapping),
],
};

Expand Down Expand Up @@ -792,10 +817,77 @@ file_extension = '{}'
Ok(())
}

/// Sets the Hypothesis group used for Gooseberry annotations
/// This opens a command-line prompt wherein the user can select creating a new group or
/// using an existing group by ID
pub async fn set_group(&mut self, group_id: Option<String>) -> color_eyre::Result<()> {
/// This opens a command-line prompt where the user can select from either creating a new group or
/// using an existing group by ID, with the option of selecting multiple groups
pub async fn get_groups(&self, api: Hypothesis) -> color_eyre::Result<HashMap<String, String>> {
let selections = &[
"Create a new Hypothesis group",
"Use existing Hypothesis groups",
];
let selection = Select::with_theme(&theme::ColorfulTheme::default())
.with_prompt("Where should gooseberry take annotations from?")
.items(&selections[..])
.interact()?;
let mut selected = HashSet::new();
if selection == 0 {
loop {
let group_name = utils::user_input("Enter a group name", Some(NAME), true, false)?;
let group_description = utils::user_input(
"Enter a group description",
Some("Gooseberry knowledge base annotations"),
true,
true,
)?;

let group_id = api
.create_group(&group_name, Some(&group_description))
.await?
.id;

selected.insert(group_id.clone());
if Confirm::with_theme(&theme::ColorfulTheme::default())
.with_prompt("Add more groups?")
.interact()?
{
continue;
} else {
break;
}
}
}
let groups = api
.get_groups(&hypothesis::groups::GroupFilters::default())
.await?;
let group_selection: Vec<_> = groups
.iter()
.map(|g| format!("{}: {}", g.id, g.name))
.collect();
let defaults: Vec<_> = groups.iter().map(|g| selected.contains(&g.id)).collect();
let mut group_name_mapping = HashMap::new();
for group_index in MultiSelect::with_theme(&theme::ColorfulTheme::default())
.with_prompt("Which groups should gooseberry use?")
.items(&group_selection[..])
.defaults(&defaults[..])
.interact()?
{
api.fetch_group(&groups[group_index].id, Vec::new())
.await
.map_err(|error| Apologize::GroupNotFound {
id: groups[group_index].id.clone(),
error,
})?;
group_name_mapping.insert(
groups[group_index].id.to_owned(),
groups[group_index].name.to_owned(),
);
}
Ok(group_name_mapping)
}

/// Sets the Hypothesis groups used for Gooseberry annotations
/// This opens a command-line prompt where the user can select from either creating a new group or
/// using an existing group by ID, with the option of selecting multiple groups
pub async fn set_groups(&mut self, group_ids: Vec<String>) -> color_eyre::Result<()> {
let (username, key) = (
self.hypothesis_username
.as_deref()
Expand All @@ -805,61 +897,22 @@ file_extension = '{}'
.ok_or_else(|| eyre!("No Hypothesis key"))?,
);
let api = Hypothesis::new(username, key)?;
if let Some(group_id) = group_id {
if api.fetch_group(&group_id, Vec::new()).await.is_ok() {
self.hypothesis_group = Some(group_id);
self.store()?;
return Ok(());
} else {
println!(
"\nGroup could not be loaded, please try again.\n\
Make sure the group exists and you are authorized to access it.\n\n"
)
if group_ids.is_empty() {
self.hypothesis_groups = self.get_groups(api).await?;
} else {
for group_id in group_ids {
let group = api
.fetch_group(&group_id, Vec::new())
.await
.map_err(|error| Apologize::GroupNotFound {
id: group_id.clone(),
error,
})?;
self.hypothesis_groups
.insert(group.id.to_owned(), group.name.to_owned());
}
}
let selections = &[
"Create a new Hypothesis group",
"Use an existing Hypothesis group",
];

let group_id = loop {
let selection = Select::with_theme(&theme::ColorfulTheme::default())
.with_prompt("Where should gooseberry take annotations from?")
.items(&selections[..])
.interact()?;

if selection == 0 {
let group_name = utils::user_input("Enter a group name", Some(NAME), true, false)?;
let group_id = Hypothesis::new(username, key)?
.create_group(&group_name, Some("Gooseberry knowledge base annotations"))
.await?
.id;
break group_id;
} else {
let groups = api
.get_groups(&hypothesis::groups::GroupFilters::default())
.await?;
let group_selection: Vec<_> = groups
.iter()
.map(|g| format!("{}: {}", g.id, g.name))
.collect();
let group_index = Select::with_theme(&theme::ColorfulTheme::default())
.with_prompt("Which group should gooseberry use?")
.items(&group_selection[..])
.interact()?;
let group_id = groups[group_index].id.to_owned();
if api.fetch_group(&group_id, Vec::new()).await.is_ok() {
break group_id;
} else {
println!(
"\nGroup could not be loaded, please try again.\n\
Make sure the group exists and you are authorized to access it.\n\n"
)
}
}
};

self.hypothesis_group = Some(group_id);
self.hypothesis_group = None;
self.store()?;
Ok(())
}
Expand Down Expand Up @@ -930,7 +983,7 @@ file_extension = '{}'
message: "The current config_file location does not seem to have write access. \
Use `export GOOSEBERRY_CONFIG=<full/path/to/config_file.toml>` to set a new location".into()
})?,
None => confy::store(NAME, None, (*self).clone()).suggestion(Apologize::ConfigError {
None => confy::store(NAME, (*self).clone()).suggestion(Apologize::ConfigError {
message: "The current config_file location does not seem to have write access. \
Use `export GOOSEBERRY_CONFIG=<full/path/to/config_file.toml>` to set a new location".into()
})?,
Expand Down
7 changes: 4 additions & 3 deletions src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use hypothesis::errors::HypothesisError;
use thiserror::Error;

/// "It claimed to have 15 functions, although it appeared that at least ten were apologizing for
Expand All @@ -10,9 +11,9 @@ pub enum Apologize {
/// Thrown when trying annotation ID doesn't match any recorded annotations
#[error("Couldn't find an annotation with ID {id:?}")]
AnnotationNotFound { id: String },
/// Thrown when trying to access an unrecorded tag
#[error("Couldn't find group {id:?}. The Group ID can be found in the URL of the group: https://hypothes.is/groups/<group_id>/<group_name>")]
GroupNotFound { id: String },
/// Thrown when trying to access an unrecorded group
#[error("Couldn't access group {id:?}: {error:?}. The Group ID can be found in the URL of the group: https://hypothes.is/groups/<group_id>/<group_name>")]
GroupNotFound { id: String, error: HypothesisError },
/// Thrown when explicit Y not received from user for destructive things
#[error("I'm a coward. Doing nothing.")]
DoingNothing,
Expand Down
15 changes: 11 additions & 4 deletions src/gooseberry/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,9 @@ pub struct Filters {
/// Only annotations with ANY of these tags (use --and to match ALL)
#[clap(long, value_delimiter = ',')]
pub tags: Vec<String>,
/// Only annotations from these groups
#[clap(long, value_delimiter = ',')]
pub groups: Vec<String>,
/// Only annotations without ANY of these tags
#[clap(long, value_delimiter = ',')]
pub exclude_tags: Vec<String>,
Expand Down Expand Up @@ -202,6 +205,7 @@ impl From<Filters> for SearchQuery {
},
quote: filters.quote,
text: filters.text,
group: filters.groups,
..SearchQuery::default()
}
}
Expand Down Expand Up @@ -232,8 +236,11 @@ pub enum ConfigCommand {
Where,
/// Change Hypothesis credentials
Authorize,
/// Change the group used for Hypothesis annotations
Group { group_id: Option<String> },
/// Change the groups used for Hypothesis annotations
Group {
#[clap(value_delimiter = ',', required = false)]
group_ids: Vec<String>,
},
/// Change options related to the knowledge base
Kb {
#[clap(subcommand)]
Expand Down Expand Up @@ -285,9 +292,9 @@ impl ConfigCommand {
let mut config = GooseberryConfig::load(config_file).await?;
config.request_credentials().await?;
}
Self::Group { group_id } => {
Self::Group { group_ids } => {
let mut config = GooseberryConfig::load(config_file).await?;
config.set_group(group_id.clone()).await?;
config.set_groups(group_ids.clone()).await?;
}
Self::Kb { cmd } => {
let mut config = GooseberryConfig::load(config_file).await?;
Expand Down
Loading

0 comments on commit aa62233

Please sign in to comment.