a lot of stuff
This commit is contained in:
+202
-34
@@ -205,6 +205,12 @@ pub struct BulkTagResponse {
|
||||
pub removed: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct BulkCorrespondentResponse {
|
||||
pub assigned: usize,
|
||||
pub removed: usize,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CorrespondentAssignmentInput {
|
||||
pub correspondent_id: Uuid,
|
||||
@@ -218,6 +224,69 @@ pub struct AssignCorrespondentsRequest {
|
||||
pub replace: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Copy, Clone, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum BulkCorrespondentAction {
|
||||
Add,
|
||||
Remove,
|
||||
}
|
||||
|
||||
fn default_bulk_correspondent_action() -> BulkCorrespondentAction {
|
||||
BulkCorrespondentAction::Add
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct BulkCorrespondentsRequest {
|
||||
pub document_ids: Vec<Uuid>,
|
||||
pub assignments: Vec<CorrespondentAssignmentInput>,
|
||||
#[serde(default = "default_bulk_correspondent_action")]
|
||||
pub action: BulkCorrespondentAction,
|
||||
}
|
||||
|
||||
fn normalize_correspondent_assignments(
|
||||
assignments: &[CorrespondentAssignmentInput],
|
||||
) -> AppResult<(Vec<(Uuid, String)>, Vec<Uuid>, Vec<String>)> {
|
||||
let mut unique_pairs: HashSet<(Uuid, String)> = HashSet::new();
|
||||
let mut normalized_pairs: Vec<(Uuid, String)> = Vec::new();
|
||||
let mut role_set: HashSet<String> = HashSet::new();
|
||||
let mut correspondent_ids: HashSet<Uuid> = HashSet::new();
|
||||
|
||||
for assignment in assignments {
|
||||
let role = normalize_role(&assignment.role);
|
||||
if role.is_empty() {
|
||||
return Err(AppError::bad_request("role must not be empty"));
|
||||
}
|
||||
if !is_valid_correspondent_role(&role) {
|
||||
return Err(AppError::bad_request(format!(
|
||||
"invalid correspondent role '{role}'. Allowed roles: {}",
|
||||
CORRESPONDENT_ROLES.join(", ")
|
||||
)));
|
||||
}
|
||||
|
||||
if !unique_pairs.insert((assignment.correspondent_id, role.clone())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
normalized_pairs.push((assignment.correspondent_id, role.clone()));
|
||||
role_set.insert(role);
|
||||
correspondent_ids.insert(assignment.correspondent_id);
|
||||
}
|
||||
|
||||
if normalized_pairs.is_empty() {
|
||||
return Err(AppError::bad_request(
|
||||
"assignments must contain at least one unique correspondent/role pair",
|
||||
));
|
||||
}
|
||||
|
||||
let mut correspondents_vec: Vec<Uuid> = correspondent_ids.into_iter().collect();
|
||||
correspondents_vec.sort();
|
||||
|
||||
let mut roles_vec: Vec<String> = role_set.into_iter().collect();
|
||||
roles_vec.sort();
|
||||
|
||||
Ok((normalized_pairs, correspondents_vec, roles_vec))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CorrespondentRoleQuery {
|
||||
pub role: String,
|
||||
@@ -850,40 +919,8 @@ pub async fn assign_correspondents(
|
||||
return Err(AppError::bad_request("assignments must not be empty"));
|
||||
}
|
||||
|
||||
let mut unique_pairs: HashSet<(Uuid, String)> = HashSet::new();
|
||||
let mut normalized_pairs: Vec<(Uuid, String)> = Vec::new();
|
||||
let mut role_set: HashSet<String> = HashSet::new();
|
||||
let mut correspondent_ids: HashSet<Uuid> = HashSet::new();
|
||||
|
||||
for assignment in &payload.assignments {
|
||||
let role = normalize_role(&assignment.role);
|
||||
if role.is_empty() {
|
||||
return Err(AppError::bad_request("role must not be empty"));
|
||||
}
|
||||
if !is_valid_correspondent_role(&role) {
|
||||
return Err(AppError::bad_request(format!(
|
||||
"invalid correspondent role '{role}'. Allowed roles: {}",
|
||||
CORRESPONDENT_ROLES.join(", ")
|
||||
)));
|
||||
}
|
||||
|
||||
if !unique_pairs.insert((assignment.correspondent_id, role.clone())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
normalized_pairs.push((assignment.correspondent_id, role.clone()));
|
||||
role_set.insert(role);
|
||||
correspondent_ids.insert(assignment.correspondent_id);
|
||||
}
|
||||
|
||||
if normalized_pairs.is_empty() {
|
||||
return Err(AppError::bad_request(
|
||||
"assignments must contain at least one unique correspondent/role pair",
|
||||
));
|
||||
}
|
||||
|
||||
let correspondents_vec: Vec<Uuid> = correspondent_ids.into_iter().collect();
|
||||
let roles_vec: Vec<String> = role_set.into_iter().collect();
|
||||
let (normalized_pairs, correspondents_vec, roles_vec) =
|
||||
normalize_correspondent_assignments(&payload.assignments)?;
|
||||
let replace = payload.replace;
|
||||
let user_id = user.user_id;
|
||||
|
||||
@@ -950,6 +987,137 @@ pub async fn assign_correspondents(
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
pub async fn bulk_assign_correspondents(
|
||||
State(state): State<AppState>,
|
||||
user: AuthenticatedUser,
|
||||
Json(payload): Json<BulkCorrespondentsRequest>,
|
||||
) -> AppResult<(StatusCode, Json<BulkCorrespondentResponse>)> {
|
||||
if payload.document_ids.is_empty() {
|
||||
return Err(AppError::bad_request("document_ids must not be empty"));
|
||||
}
|
||||
if payload.assignments.is_empty() {
|
||||
return Err(AppError::bad_request("assignments must not be empty"));
|
||||
}
|
||||
|
||||
let mut document_ids = payload.document_ids;
|
||||
document_ids.sort();
|
||||
document_ids.dedup();
|
||||
|
||||
let (normalized_pairs, correspondents_vec, roles_vec) =
|
||||
normalize_correspondent_assignments(&payload.assignments)?;
|
||||
let action = payload.action;
|
||||
let user_id = user.user_id;
|
||||
|
||||
let mut conn = state.db()?;
|
||||
let (assigned, removed) = conn.transaction::<(usize, usize), AppError, _>(|conn| {
|
||||
let docs: Vec<(Uuid, Option<NaiveDateTime>)> = documents::table
|
||||
.filter(documents::id.eq_any(&document_ids))
|
||||
.select((documents::id, documents::deleted_at))
|
||||
.load(conn)?;
|
||||
|
||||
if docs.len() != document_ids.len() {
|
||||
return Err(AppError::bad_request(
|
||||
"one or more documents do not exist or are inaccessible",
|
||||
));
|
||||
}
|
||||
|
||||
if docs.iter().any(|(_, deleted)| deleted.is_some()) {
|
||||
return Err(AppError::bad_request(
|
||||
"cannot assign correspondents to deleted documents",
|
||||
));
|
||||
}
|
||||
|
||||
if !correspondents_vec.is_empty() {
|
||||
let existing: Vec<Correspondent> = correspondents::table
|
||||
.filter(correspondents::id.eq_any(&correspondents_vec))
|
||||
.load(conn)?;
|
||||
if existing.len() != correspondents_vec.len() {
|
||||
return Err(AppError::bad_request(
|
||||
"one or more correspondents do not exist",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
match action {
|
||||
BulkCorrespondentAction::Add => {
|
||||
let mut removed = 0;
|
||||
if !roles_vec.is_empty() {
|
||||
removed = diesel::delete(
|
||||
document_correspondents::table
|
||||
.filter(document_correspondents::document_id.eq_any(&document_ids))
|
||||
.filter(document_correspondents::role.eq_any(&roles_vec)),
|
||||
)
|
||||
.execute(conn)?;
|
||||
}
|
||||
|
||||
let mut new_rows = Vec::with_capacity(document_ids.len() * normalized_pairs.len());
|
||||
for doc_id in &document_ids {
|
||||
for (correspondent_id, role) in &normalized_pairs {
|
||||
new_rows.push(NewDocumentCorrespondent {
|
||||
document_id: *doc_id,
|
||||
correspondent_id: *correspondent_id,
|
||||
role: role.clone(),
|
||||
assigned_by: Some(user_id),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let assigned = if new_rows.is_empty() {
|
||||
0
|
||||
} else {
|
||||
diesel::insert_into(document_correspondents::table)
|
||||
.values(&new_rows)
|
||||
.on_conflict_do_nothing()
|
||||
.execute(conn)?
|
||||
};
|
||||
|
||||
if assigned > 0 || removed > 0 {
|
||||
diesel::update(documents::table.filter(documents::id.eq_any(&document_ids)))
|
||||
.set(documents::updated_at.eq(Utc::now().naive_utc()))
|
||||
.execute(conn)?;
|
||||
}
|
||||
|
||||
Ok((assigned, removed))
|
||||
}
|
||||
BulkCorrespondentAction::Remove => {
|
||||
let mut removed = 0;
|
||||
if !normalized_pairs.is_empty() {
|
||||
let mut grouped: HashMap<String, Vec<Uuid>> = HashMap::new();
|
||||
for (correspondent_id, role) in &normalized_pairs {
|
||||
grouped
|
||||
.entry(role.clone())
|
||||
.or_default()
|
||||
.push(*correspondent_id);
|
||||
}
|
||||
|
||||
for (role, ids) in grouped {
|
||||
removed += diesel::delete(
|
||||
document_correspondents::table
|
||||
.filter(document_correspondents::document_id.eq_any(&document_ids))
|
||||
.filter(document_correspondents::role.eq(role.as_str()))
|
||||
.filter(document_correspondents::correspondent_id.eq_any(&ids)),
|
||||
)
|
||||
.execute(conn)?;
|
||||
}
|
||||
}
|
||||
|
||||
if removed > 0 {
|
||||
diesel::update(documents::table.filter(documents::id.eq_any(&document_ids)))
|
||||
.set(documents::updated_at.eq(Utc::now().naive_utc()))
|
||||
.execute(conn)?;
|
||||
}
|
||||
|
||||
Ok((0, removed))
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
Json(BulkCorrespondentResponse { assigned, removed }),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn remove_correspondent(
|
||||
State(state): State<AppState>,
|
||||
Path((document_id, correspondent_id)): Path<(Uuid, Uuid)>,
|
||||
|
||||
Reference in New Issue
Block a user