fa14a5ca8c
- GIN indexes on students/teachers (jsonb_path_ops) for fast user lookups - Composite index on clazz (is_repeat, start_from, end_by) for date range queries - Indexes on clazz_repeat (clazz_id, repeat_start, repeat_end) for joins Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
988 lines
31 KiB
Rust
988 lines
31 KiB
Rust
use anyhow::anyhow;
|
|
use axum::extract::{Path, Query, State};
|
|
use axum::Json;
|
|
use diesel::PgConnection;
|
|
use htycommons::common::{
|
|
current_local_datetime, get_some_from_query_params, string_to_date, string_to_datetime, HtyErr,
|
|
HtyErrCode, HtyResponse,
|
|
};
|
|
use htycommons::db::*;
|
|
use htycommons::jwt::jwt_decode_token;
|
|
use htycommons::uuid;
|
|
use htycommons::web::{
|
|
wrap_json_anyhow_err, wrap_json_ok_resp, AuthorizationHeader, HtyHostHeader,
|
|
HtySudoerTokenHeader,
|
|
};
|
|
use htykc_models::models::{
|
|
Clazz, ClazzAuditLog, ClazzRepeat, NewClazzAuditLog, ReqClazz, ReqClazzRepeat, ReqClazzWithRepeat,
|
|
};
|
|
use std::collections::HashMap;
|
|
use std::ops::DerefMut;
|
|
use std::sync::Arc;
|
|
use tracing::{debug, error};
|
|
|
|
fn current_org_id_from_auth(token: &AuthorizationHeader) -> Option<String> {
|
|
jwt_decode_token(&(*token).clone())
|
|
.ok()
|
|
.and_then(|decoded| decoded.current_org_id)
|
|
}
|
|
|
|
fn hty_id_from_auth(token: &AuthorizationHeader) -> Option<String> {
|
|
jwt_decode_token(&(*token).clone())
|
|
.ok()
|
|
.and_then(|decoded| decoded.hty_id)
|
|
}
|
|
|
|
fn write_clazz_audit_log(
|
|
db_pool: &Arc<DbState>,
|
|
clazz_id: &str,
|
|
action: &str,
|
|
operator_hty_id: &str,
|
|
changes: Option<serde_json::Value>,
|
|
org_id: Option<String>,
|
|
) {
|
|
if let Ok(conn) = fetch_db_conn(db_pool) {
|
|
let _ = ClazzAuditLog::insert(
|
|
&NewClazzAuditLog {
|
|
id: uuid(),
|
|
clazz_id: clazz_id.to_string(),
|
|
action: action.to_string(),
|
|
operator_hty_id: operator_hty_id.to_string(),
|
|
operator_name: None,
|
|
changes,
|
|
created_at: current_local_datetime(),
|
|
org_id,
|
|
},
|
|
extract_conn(conn).deref_mut(),
|
|
);
|
|
}
|
|
}
|
|
|
|
pub async fn find_all_non_repeatable_within_date_range(
|
|
sudoer: HtySudoerTokenHeader,
|
|
host: HtyHostHeader,
|
|
auth: AuthorizationHeader,
|
|
State(db_pool): State<Arc<DbState>>,
|
|
Query(params): Query<HashMap<String, String>>,
|
|
) -> Json<HtyResponse<Vec<ReqClazz>>> {
|
|
debug!(
|
|
"find_all_non_repeatable_within_date_range -> starts, params: {:?}",
|
|
params
|
|
);
|
|
|
|
match raw_find_all_non_repeatable_within_date_range(
|
|
auth, sudoer, host, db_pool, ¶ms,
|
|
) {
|
|
Ok(ok) => wrap_json_ok_resp(ok),
|
|
Err(e) => {
|
|
error!(
|
|
"find_all_non_repeatable_within_date_range -> failed to find kecheng, e: {}",
|
|
e
|
|
);
|
|
wrap_json_anyhow_err(e)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn raw_find_all_non_repeatable_within_date_range(
|
|
token: AuthorizationHeader,
|
|
_sudoer: HtySudoerTokenHeader,
|
|
_host: HtyHostHeader,
|
|
db_pool: Arc<DbState>,
|
|
params: &HashMap<String, String>,
|
|
) -> anyhow::Result<Vec<ReqClazz>> {
|
|
let start_from = string_to_date(&get_some_from_query_params::<String>("start_from", ¶ms))?;
|
|
let end_by = string_to_date(&get_some_from_query_params::<String>("end_by", ¶ms))?;
|
|
|
|
debug!(
|
|
"raw_find_all_non_repeatable_within_date_range -> start_from: {:?}",
|
|
start_from
|
|
);
|
|
debug!(
|
|
"raw_find_all_non_repeatable_within_date_range -> end_by: {:?}",
|
|
end_by
|
|
);
|
|
|
|
if start_from.is_none() || end_by.is_none() {
|
|
return Err(anyhow!(HtyErr {
|
|
code: HtyErrCode::WebErr,
|
|
reason: Some("start_from or end_by is none".into()),
|
|
}));
|
|
}
|
|
|
|
let some_kechengs = Clazz::find_all_non_repeatable_by_date_range(
|
|
start_from
|
|
.as_ref()
|
|
.ok_or_else(|| anyhow!("start_from is required"))?,
|
|
end_by
|
|
.as_ref()
|
|
.ok_or_else(|| anyhow!("end_by is required"))?,
|
|
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
|
)?;
|
|
|
|
debug!(
|
|
"raw_find_all_non_repeatable_within_date_range -> some_kechengs: {:?}",
|
|
some_kechengs
|
|
);
|
|
|
|
if let Some(kechengs) = some_kechengs {
|
|
let current_org_id = current_org_id_from_auth(&token);
|
|
let res: Vec<ReqClazz> = kechengs
|
|
.iter()
|
|
.filter(|clazz| {
|
|
if let Some(org_id) = ¤t_org_id {
|
|
clazz.org_id.as_ref() == Some(org_id)
|
|
} else {
|
|
true
|
|
}
|
|
})
|
|
.map(|kc| kc.to_req())
|
|
.collect();
|
|
Ok(res)
|
|
} else {
|
|
Ok(vec![])
|
|
}
|
|
}
|
|
|
|
pub async fn find_all_non_repeatable_within_date_range_by_hty_id(
|
|
sudoer: HtySudoerTokenHeader,
|
|
host: HtyHostHeader,
|
|
auth: AuthorizationHeader,
|
|
State(db_pool): State<Arc<DbState>>,
|
|
Query(params): Query<HashMap<String, String>>,
|
|
) -> Json<HtyResponse<Vec<ReqClazz>>> {
|
|
debug!(
|
|
"find_all_non_repeatable_within_date_range_by_hty_id -> starts, params: {:?}",
|
|
params
|
|
);
|
|
|
|
match raw_find_all_non_repeatable_within_date_range_by_hty_id(
|
|
auth, sudoer, host, db_pool, ¶ms,
|
|
) {
|
|
Ok(ok) => wrap_json_ok_resp(ok),
|
|
Err(e) => {
|
|
error!(
|
|
"find_all_non_repeatable_within_date_range_by_hty_id -> failed to find kecheng, e: {}",
|
|
e
|
|
);
|
|
wrap_json_anyhow_err(e)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn raw_find_all_non_repeatable_within_date_range_by_hty_id(
|
|
token: AuthorizationHeader,
|
|
_sudoer: HtySudoerTokenHeader,
|
|
_host: HtyHostHeader,
|
|
db_pool: Arc<DbState>,
|
|
params: &HashMap<String, String>,
|
|
) -> anyhow::Result<Vec<ReqClazz>> {
|
|
let start_from =
|
|
string_to_date(&get_some_from_query_params::<String>("start_from", ¶ms))?;
|
|
let end_by = string_to_date(&get_some_from_query_params::<String>("end_by", ¶ms))?;
|
|
|
|
debug!(
|
|
"raw_find_all_non_repeatable_within_date_range_by_hty_id -> start_from: {:?}",
|
|
start_from
|
|
);
|
|
debug!(
|
|
"raw_find_all_non_repeatable_within_date_range_by_hty_id -> end_by: {:?}",
|
|
end_by
|
|
);
|
|
|
|
let id_user = get_some_from_query_params::<String>("hty_id", ¶ms);
|
|
|
|
if start_from.is_none() || end_by.is_none() {
|
|
return Err(anyhow!(HtyErr {
|
|
code: HtyErrCode::WebErr,
|
|
reason: Some("start_from or end_by is none".into()),
|
|
}));
|
|
}
|
|
|
|
if id_user.is_none() {
|
|
return Err(anyhow!(HtyErr {
|
|
code: HtyErrCode::WebErr,
|
|
reason: Some("hty_id is none".into()),
|
|
}));
|
|
}
|
|
|
|
let some_kechengs = Clazz::find_all_non_repeatable_by_date_range_and_user_id(
|
|
id_user
|
|
.as_ref()
|
|
.ok_or_else(|| anyhow!("id_user is required"))?,
|
|
start_from
|
|
.as_ref()
|
|
.ok_or_else(|| anyhow!("start_from is required"))?,
|
|
end_by
|
|
.as_ref()
|
|
.ok_or_else(|| anyhow!("end_by is required"))?,
|
|
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
|
)?;
|
|
|
|
debug!(
|
|
"raw_find_all_non_repeatable_within_date_range_by_hty_id -> {:?}",
|
|
some_kechengs
|
|
);
|
|
|
|
if let Some(kechengs) = some_kechengs {
|
|
let current_org_id = current_org_id_from_auth(&token);
|
|
let res: Vec<ReqClazz> = kechengs
|
|
.iter()
|
|
.filter(|clazz| {
|
|
if let Some(org_id) = ¤t_org_id {
|
|
clazz.org_id.as_ref() == Some(org_id)
|
|
} else {
|
|
true
|
|
}
|
|
})
|
|
.map(|kc| kc.to_req())
|
|
.collect();
|
|
Ok(res)
|
|
} else {
|
|
Ok(vec![])
|
|
}
|
|
}
|
|
|
|
pub async fn find_all_repeatable_within_date_range(
|
|
sudoer: HtySudoerTokenHeader,
|
|
host: HtyHostHeader,
|
|
auth: AuthorizationHeader,
|
|
State(db_pool): State<Arc<DbState>>,
|
|
Query(params): Query<HashMap<String, String>>,
|
|
) -> Json<HtyResponse<Vec<ReqClazzWithRepeat>>> {
|
|
debug!(
|
|
"find_all_repeatable_within_date_range -> starts, params: {:?}",
|
|
params
|
|
);
|
|
|
|
match raw_find_all_repeatable_within_date_range(auth, sudoer, host, db_pool, ¶ms) {
|
|
Ok(kechengs) => wrap_json_ok_resp(kechengs),
|
|
Err(e) => {
|
|
error!(
|
|
"find_all_repeatable_within_date_range -> failed to find kecheng, e: {}",
|
|
e
|
|
);
|
|
wrap_json_anyhow_err(e)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn raw_find_all_repeatable_within_date_range(
|
|
token: AuthorizationHeader,
|
|
_sudoer: HtySudoerTokenHeader,
|
|
_host: HtyHostHeader,
|
|
db_pool: Arc<DbState>,
|
|
params: &HashMap<String, String>,
|
|
) -> anyhow::Result<Vec<ReqClazzWithRepeat>> {
|
|
let start_from = string_to_date(&get_some_from_query_params::<String>("start_from", ¶ms))?;
|
|
let end_by = string_to_date(&get_some_from_query_params::<String>("end_by", ¶ms))?;
|
|
|
|
debug!(
|
|
"raw_find_all_repeatable_within_date_range -> start_from: {:?}",
|
|
start_from
|
|
);
|
|
debug!(
|
|
"raw_find_all_repeatable_within_date_range -> end_by: {:?}",
|
|
end_by
|
|
);
|
|
|
|
if start_from.is_none() || end_by.is_none() {
|
|
return Err(anyhow!(HtyErr {
|
|
code: HtyErrCode::WebErr,
|
|
reason: Some("start_from or end_by is none".into()),
|
|
}));
|
|
}
|
|
|
|
let some_kechengs = Clazz::find_all_repeatable_by_date_range(
|
|
start_from
|
|
.as_ref()
|
|
.ok_or_else(|| anyhow!("start_from is required"))?,
|
|
end_by
|
|
.as_ref()
|
|
.ok_or_else(|| anyhow!("end_by is required"))?,
|
|
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
|
)?;
|
|
|
|
debug!(
|
|
"raw_find_all_repeatable_within_date_range -> some_kechengs: {:?}",
|
|
some_kechengs
|
|
);
|
|
|
|
if let Some(kechengs) = some_kechengs {
|
|
let current_org_id = current_org_id_from_auth(&token);
|
|
let filtered_kechengs: Vec<ReqClazzWithRepeat> = kechengs
|
|
.into_iter()
|
|
.filter(|clazz| {
|
|
if let Some(org_id) = ¤t_org_id {
|
|
clazz.clz_org_id.as_ref() == Some(org_id)
|
|
} else {
|
|
true
|
|
}
|
|
})
|
|
.collect();
|
|
debug!(
|
|
"raw_find_all_repeatable_within_date_range -> kechengs.len(): {:?}",
|
|
filtered_kechengs.len()
|
|
);
|
|
Ok(filtered_kechengs)
|
|
} else {
|
|
Ok(vec![])
|
|
}
|
|
}
|
|
|
|
pub async fn find_all_repeatable_within_date_range_by_hty_id(
|
|
sudoer: HtySudoerTokenHeader,
|
|
host: HtyHostHeader,
|
|
auth: AuthorizationHeader,
|
|
State(db_pool): State<Arc<DbState>>,
|
|
Query(params): Query<HashMap<String, String>>,
|
|
) -> Json<HtyResponse<Vec<ReqClazzWithRepeat>>> {
|
|
debug!(
|
|
"find_all_repeatable_within_date_range_by_hty_id -> starts, params: {:?}",
|
|
params
|
|
);
|
|
|
|
match raw_find_all_repeatable_within_date_range_by_hty_id(
|
|
auth, sudoer, host, db_pool, ¶ms,
|
|
) {
|
|
Ok(ok) => wrap_json_ok_resp(ok),
|
|
Err(e) => {
|
|
error!(
|
|
"find_all_repeatable_within_date_range_by_hty_id -> failed to find kecheng, e: {}",
|
|
e
|
|
);
|
|
wrap_json_anyhow_err(e)
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* 找到前端给定时间范围内的所有kecheng数据
|
|
* 本周的重复课程如果存在,返回重复课程对应的sudoer课程
|
|
* 1. hty_user_id / 哪个用的课程
|
|
* 2. start_from / end_by
|
|
* 3. 实体kecheng数据
|
|
* 4. 需要显示在本周的重复课程的sudoer课程 (kecheng_repeat_status == `OPEN` && is_repeat == True && repeat_end 在本周范围内 or 为空)
|
|
* htykc_moicen=# select kecheng.is_repeat from kecheng join kecheng_repeat on kecheng.id = kecheng_repeat.clazz_id where kecheng.is_repeat is not null;
|
|
*
|
|
* a. 返回的kecheng里,如果`id == root_id`那么此条数据就是sudoer数据.
|
|
* b. 如果此条kecheng的`start_from`和`end_by`落在本周,则说明是本周数据
|
|
*/
|
|
fn raw_find_all_repeatable_within_date_range_by_hty_id(
|
|
token: AuthorizationHeader,
|
|
_sudoer: HtySudoerTokenHeader,
|
|
_host: HtyHostHeader,
|
|
db_pool: Arc<DbState>,
|
|
params: &HashMap<String, String>,
|
|
) -> anyhow::Result<Vec<ReqClazzWithRepeat>> {
|
|
let start_from =
|
|
string_to_date(&get_some_from_query_params::<String>("start_from", ¶ms))?;
|
|
let end_by = string_to_date(&get_some_from_query_params::<String>("end_by", ¶ms))?;
|
|
|
|
debug!(
|
|
"raw_find_all_repeatable_within_date_range_by_hty_id -> start_from: {:?}",
|
|
start_from
|
|
);
|
|
debug!(
|
|
"raw_find_all_repeatable_within_date_range_by_hty_id -> end_by: {:?}",
|
|
end_by
|
|
);
|
|
|
|
let id_user = get_some_from_query_params::<String>("hty_id", ¶ms);
|
|
|
|
if start_from.is_none() || end_by.is_none() {
|
|
return Err(anyhow!(HtyErr {
|
|
code: HtyErrCode::WebErr,
|
|
reason: Some("start_from or end_by is none".into()),
|
|
}));
|
|
}
|
|
|
|
if id_user.is_none() {
|
|
return Err(anyhow!(HtyErr {
|
|
code: HtyErrCode::WebErr,
|
|
reason: Some("hty_id is none".into()),
|
|
}));
|
|
}
|
|
|
|
let some_kechengs = Clazz::find_all_repeatable_by_date_range_and_user_id(
|
|
id_user
|
|
.as_ref()
|
|
.ok_or_else(|| anyhow!("id_user is required"))?,
|
|
start_from
|
|
.as_ref()
|
|
.ok_or_else(|| anyhow!("start_from is required"))?,
|
|
end_by
|
|
.as_ref()
|
|
.ok_or_else(|| anyhow!("end_by is required"))?,
|
|
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
|
)?;
|
|
|
|
debug!(
|
|
"raw_find_all_repeatable_within_date_range_by_hty_id -> some_kechengs: {:?}",
|
|
some_kechengs
|
|
);
|
|
|
|
if let Some(kechengs) = some_kechengs {
|
|
let current_org_id = current_org_id_from_auth(&token);
|
|
let filtered_kechengs: Vec<ReqClazzWithRepeat> = kechengs
|
|
.into_iter()
|
|
.filter(|clazz| {
|
|
if let Some(org_id) = ¤t_org_id {
|
|
clazz.clz_org_id.as_ref() == Some(org_id)
|
|
} else {
|
|
true
|
|
}
|
|
})
|
|
.collect();
|
|
debug!(
|
|
"raw_find_all_repeatable_within_date_range_by_hty_id -> kechengs.len(): {:?}",
|
|
filtered_kechengs.len()
|
|
);
|
|
Ok(filtered_kechengs)
|
|
} else {
|
|
Ok(vec![])
|
|
}
|
|
}
|
|
|
|
pub async fn find_by_daka_ids(
|
|
sudoer: HtySudoerTokenHeader,
|
|
host: HtyHostHeader,
|
|
auth: AuthorizationHeader,
|
|
State(db_pool): State<Arc<DbState>>,
|
|
daka_ids: Json<Vec<String>>,
|
|
) -> Json<HtyResponse<Vec<ReqClazz>>> {
|
|
debug!("find_by_daka_ids -> starts: {:?}", daka_ids);
|
|
match raw_find_by_daka_ids(auth, sudoer, host, db_pool, &daka_ids) {
|
|
Ok(ok) => wrap_json_ok_resp(ok),
|
|
Err(e) => {
|
|
error!(
|
|
"find_by_daka_ids -> failed to find kecheng, e: {}",
|
|
e
|
|
);
|
|
wrap_json_anyhow_err(e)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn raw_find_by_daka_ids(
|
|
_auth: AuthorizationHeader,
|
|
_sudoer: HtySudoerTokenHeader,
|
|
_host: HtyHostHeader,
|
|
db_pool: Arc<DbState>,
|
|
daka_ids: &Vec<String>,
|
|
) -> anyhow::Result<Vec<ReqClazz>> {
|
|
let db_kechengs = Clazz::find_by_daka_ids(
|
|
daka_ids,
|
|
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
|
)?;
|
|
debug!(
|
|
"raw_find_by_daka_ids -> db_kechengs: {:?}",
|
|
db_kechengs
|
|
);
|
|
|
|
let req_kechengs = db_kechengs.into_iter().map(|item| item.to_req()).collect();
|
|
|
|
debug!(
|
|
"raw_find_by_daka_ids -> req_kechengs: {:?}",
|
|
req_kechengs
|
|
);
|
|
Ok(req_kechengs)
|
|
}
|
|
|
|
pub async fn find_clazz_repeat_by_id(
|
|
sudoer: HtySudoerTokenHeader,
|
|
host: HtyHostHeader,
|
|
auth: AuthorizationHeader,
|
|
State(db_pool): State<Arc<DbState>>,
|
|
Path(id): Path<String>,
|
|
) -> Json<HtyResponse<ReqClazzRepeat>> {
|
|
debug!("find_clazz_repeat_by_id -> starts: {:?}", id);
|
|
match raw_find_clazz_repeat_by_id(auth, sudoer, host, db_pool, &id) {
|
|
Ok(ok) => wrap_json_ok_resp(ok),
|
|
Err(e) => {
|
|
error!(
|
|
"find_clazz_repeat_by_id -> failed to find kecheng repeat, e: {}",
|
|
e
|
|
);
|
|
wrap_json_anyhow_err(e)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn raw_find_clazz_repeat_by_id(
|
|
_auth: AuthorizationHeader,
|
|
_sudoer: HtySudoerTokenHeader,
|
|
_host: HtyHostHeader,
|
|
db_pool: Arc<DbState>,
|
|
kecheng_repeat_id: &String,
|
|
) -> anyhow::Result<ReqClazzRepeat> {
|
|
let res = ClazzRepeat::find_by_id(
|
|
kecheng_repeat_id,
|
|
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
|
)?;
|
|
if let Some(kecheng_repeat) = res {
|
|
return Ok(kecheng_repeat.to_req());
|
|
} else {
|
|
return Err(anyhow!(HtyErr {
|
|
code: HtyErrCode::NullErr,
|
|
reason: Some(format!(
|
|
"raw_find_clazz_repeat_by_id -> failed to find record according to id :{:?}.",
|
|
kecheng_repeat_id
|
|
)),
|
|
}));
|
|
}
|
|
}
|
|
|
|
pub async fn find_clazz_by_hty_id(
|
|
sudoer: HtySudoerTokenHeader,
|
|
host: HtyHostHeader,
|
|
auth: AuthorizationHeader,
|
|
State(db_pool): State<Arc<DbState>>,
|
|
Query(params): Query<HashMap<String, String>>,
|
|
) -> Json<HtyResponse<Vec<ReqClazz>>> {
|
|
debug!("find_clazz_by_hty_id -> starts: {:?}", params);
|
|
match raw_find_clazz_by_hty_id(auth, sudoer, host, db_pool, ¶ms) {
|
|
Ok(ok) => wrap_json_ok_resp(ok),
|
|
Err(e) => {
|
|
error!(
|
|
"find_clazz_by_hty_id -> failed to find kechengs, e: {}",
|
|
e
|
|
);
|
|
wrap_json_anyhow_err(e)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn raw_find_clazz_by_hty_id(
|
|
auth: AuthorizationHeader,
|
|
_sudoer: HtySudoerTokenHeader,
|
|
_host: HtyHostHeader,
|
|
db_pool: Arc<DbState>,
|
|
params: &HashMap<String, String>,
|
|
) -> anyhow::Result<Vec<ReqClazz>> {
|
|
let start_date =
|
|
string_to_datetime(&get_some_from_query_params::<String>("start_date", ¶ms))?;
|
|
let end_date = string_to_datetime(&get_some_from_query_params::<String>("end_date", ¶ms))?;
|
|
|
|
debug!(
|
|
"raw_find_clazz_by_hty_id -> start_date: {:?}",
|
|
start_date
|
|
);
|
|
debug!("raw_find_clazz_by_hty_id -> end_date: {:?}", end_date);
|
|
|
|
if start_date.is_none() || end_date.is_none() {
|
|
return Err(anyhow!(HtyErr {
|
|
code: HtyErrCode::WebErr,
|
|
reason: Some("start_date or end_date is none".into()),
|
|
}));
|
|
}
|
|
let id_user = get_some_from_query_params::<String>("hty_id", ¶ms);
|
|
|
|
if id_user.is_none() {
|
|
return Err(anyhow!(HtyErr {
|
|
code: HtyErrCode::WebErr,
|
|
reason: Some("hty_id is none".into()),
|
|
}));
|
|
}
|
|
|
|
let current_org_id = current_org_id_from_auth(&auth);
|
|
let kechengs = Clazz::find_by_user_id(
|
|
id_user
|
|
.as_ref()
|
|
.ok_or_else(|| anyhow!("id_user is required"))?,
|
|
&start_date,
|
|
&end_date,
|
|
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
|
)?;
|
|
|
|
let res = kechengs
|
|
.into_iter()
|
|
.filter(|clazz| {
|
|
if let Some(org_id) = ¤t_org_id {
|
|
clazz.org_id.as_ref() == Some(org_id)
|
|
} else {
|
|
true
|
|
}
|
|
})
|
|
.map(|item| item.to_req())
|
|
.collect();
|
|
debug!("raw_find_clazz_by_hty_id -> {:?}", res);
|
|
Ok(res)
|
|
}
|
|
|
|
pub async fn update_clazz(
|
|
sudoer: HtySudoerTokenHeader,
|
|
host: HtyHostHeader,
|
|
auth: AuthorizationHeader,
|
|
State(db_pool): State<Arc<DbState>>,
|
|
Json(in_kecheng): Json<ReqClazz>,
|
|
) -> Json<HtyResponse<()>> {
|
|
debug!("update_clazz -> in_kecheng: {:?}", in_kecheng);
|
|
match raw_update_clazz(auth, sudoer, host, db_pool, &in_kecheng).await {
|
|
Ok(ok) => wrap_json_ok_resp(ok),
|
|
Err(e) => {
|
|
error!("update_clazz -> failed to update clazz, e: {}", e);
|
|
wrap_json_anyhow_err(e)
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn raw_update_clazz(
|
|
token: AuthorizationHeader,
|
|
_sudoer: HtySudoerTokenHeader,
|
|
_host: HtyHostHeader,
|
|
db_pool: Arc<DbState>,
|
|
in_kecheng: &ReqClazz,
|
|
) -> anyhow::Result<()> {
|
|
if in_kecheng.id.is_none() {
|
|
return Err(anyhow!(HtyErr {
|
|
code: HtyErrCode::WebErr,
|
|
reason: Some("clazz_id is none".into()),
|
|
}));
|
|
}
|
|
|
|
let id_kecheng = in_kecheng
|
|
.id
|
|
.clone()
|
|
.ok_or_else(|| anyhow!("id is required"))?;
|
|
let mut db_kecheng = Clazz::find_by_id(
|
|
&id_kecheng,
|
|
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
|
)?;
|
|
let current_org_id = current_org_id_from_auth(&token);
|
|
if let Some(org_id) = ¤t_org_id {
|
|
if db_kecheng.org_id.as_ref() != Some(org_id) {
|
|
return Err(anyhow!(HtyErr {
|
|
code: HtyErrCode::AuthenticationFailed,
|
|
reason: Some("clazz does not belong to current organization".to_string()),
|
|
}));
|
|
}
|
|
}
|
|
|
|
if let Some(clazz_name) = &in_kecheng.clazz_name {
|
|
db_kecheng.clazz_name = clazz_name.clone();
|
|
}
|
|
if let Some(clazz_status) = &in_kecheng.clazz_status {
|
|
db_kecheng.clazz_status = clazz_status.clone();
|
|
}
|
|
if let Some(start_from) = &in_kecheng.start_from {
|
|
db_kecheng.start_from = start_from.clone();
|
|
}
|
|
if let Some(end_by) = &in_kecheng.end_by {
|
|
db_kecheng.end_by = end_by.clone();
|
|
}
|
|
if let Some(root_id) = &in_kecheng.root_id {
|
|
db_kecheng.root_id = root_id.clone();
|
|
}
|
|
if let Some(parent_id) = &in_kecheng.parent_id {
|
|
db_kecheng.parent_id = parent_id.clone();
|
|
}
|
|
|
|
db_kecheng.clazz_desc = in_kecheng.clazz_desc.clone();
|
|
db_kecheng.duration = in_kecheng.duration.clone();
|
|
db_kecheng.clazz_type = in_kecheng.clazz_type.clone();
|
|
db_kecheng.students = in_kecheng.students.clone();
|
|
db_kecheng.teachers = in_kecheng.teachers.clone();
|
|
db_kecheng.jihuas = in_kecheng.jihuas.clone();
|
|
db_kecheng.dakas = in_kecheng.dakas.clone();
|
|
db_kecheng.is_delete = in_kecheng.is_delete.clone();
|
|
db_kecheng.is_repeat = in_kecheng.is_repeat.clone();
|
|
db_kecheng.course_sections = in_kecheng.course_sections.clone();
|
|
db_kecheng.is_notified = in_kecheng.is_notified.clone();
|
|
|
|
debug!("raw_update_clazz -> {:?}", db_kecheng);
|
|
|
|
let _ = Clazz::update(
|
|
&db_kecheng,
|
|
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
|
)?;
|
|
|
|
// Audit log
|
|
let operator_id = hty_id_from_auth(&token).unwrap_or_default();
|
|
let action = if in_kecheng.is_delete == Some(true) { "DELETE" } else { "UPDATE" };
|
|
write_clazz_audit_log(
|
|
&db_pool,
|
|
&id_kecheng,
|
|
action,
|
|
&operator_id,
|
|
Some(serde_json::json!({
|
|
"is_delete": in_kecheng.is_delete,
|
|
"clazz_name": in_kecheng.clazz_name,
|
|
"start_from": in_kecheng.start_from,
|
|
"end_by": in_kecheng.end_by,
|
|
})),
|
|
current_org_id,
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/*async fn find_group_users_by_group_id(_p0: &String, _p1: &HtyHostHeader, _p2: &HtySudoerTokenHeader) -> anyhow::Result<Option<Vec<ReqClazzUser>>> {
|
|
unimplemented!()
|
|
}
|
|
*/
|
|
|
|
fn raw_create_clazz_with_repeat_tx(
|
|
params: HashMap<String, (Clazz, Option<ClazzRepeat>)>,
|
|
db_pool: Arc<DbState>,
|
|
) -> anyhow::Result<ReqClazz> {
|
|
let task = move |in_params: Option<HashMap<String, (Clazz, Option<ClazzRepeat>)>>,
|
|
conn: &mut PgConnection|
|
|
-> anyhow::Result<ReqClazz> {
|
|
let the_params = in_params.ok_or_else(|| anyhow!("params is required"))?;
|
|
let (to_create_kecheng, to_create_kecheng_repeat) = the_params
|
|
.clone()
|
|
.get("params")
|
|
.ok_or_else(|| anyhow!("params key not found"))?
|
|
.clone();
|
|
|
|
let created_kecheng = Clazz::create(&to_create_kecheng, conn)?;
|
|
debug!(
|
|
"raw_create_clazz_with_repeat_tx -> created_kecheng: {:?}",
|
|
created_kecheng
|
|
);
|
|
let mut created_kecheng_repeat = None;
|
|
if let Some(kecheng_repeat) = &to_create_kecheng_repeat {
|
|
created_kecheng_repeat = Some(ClazzRepeat::create(kecheng_repeat, conn)?);
|
|
debug!(
|
|
"raw_create_clazz_with_repeat_tx -> created_kecheng_repeat: {:?}",
|
|
created_kecheng_repeat
|
|
);
|
|
}
|
|
|
|
let out =
|
|
ReqClazz::from_clazz_and_repeat(&Some(created_kecheng), &created_kecheng_repeat);
|
|
debug!("raw_create_clazz_with_repeat_tx -> out: {:?}", out);
|
|
Ok(out)
|
|
};
|
|
|
|
exec_read_write_task(
|
|
Box::new(task),
|
|
Some(params),
|
|
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
|
)
|
|
}
|
|
|
|
pub async fn create_clazz_with_repeat(
|
|
sudoer: HtySudoerTokenHeader,
|
|
host: HtyHostHeader,
|
|
auth: AuthorizationHeader,
|
|
State(db_pool): State<Arc<DbState>>,
|
|
Json(in_kecheng): Json<ReqClazz>,
|
|
) -> Json<HtyResponse<ReqClazz>> {
|
|
debug!(
|
|
"create_clazz_with_repeat -> in_kecheng: {:?}",
|
|
in_kecheng
|
|
);
|
|
match raw_create_clazz_with_repeat(auth, sudoer, host, db_pool, &in_kecheng).await {
|
|
Ok(out) => wrap_json_ok_resp(out),
|
|
Err(e) => {
|
|
error!(
|
|
"create_clazz_with_repeat -> failed to create clazz, e: {}",
|
|
e
|
|
);
|
|
wrap_json_anyhow_err(e)
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn raw_create_clazz_with_repeat(
|
|
token: AuthorizationHeader,
|
|
_sudoer: HtySudoerTokenHeader,
|
|
_host: HtyHostHeader,
|
|
db_pool: Arc<DbState>,
|
|
in_kecheng: &ReqClazz,
|
|
) -> anyhow::Result<ReqClazz> {
|
|
if in_kecheng.clazz_name.is_none()
|
|
|| in_kecheng.start_from.is_none()
|
|
|| in_kecheng.end_by.is_none()
|
|
|| in_kecheng.duration.is_none()
|
|
{
|
|
return Err(anyhow!(HtyErr {
|
|
code: HtyErrCode::WebErr,
|
|
reason: Some(
|
|
"clazz_name or start_from or end_by or duration or users is none".into(),
|
|
),
|
|
}));
|
|
}
|
|
|
|
let id_user = jwt_decode_token(&(*token).clone())?
|
|
.hty_id
|
|
.ok_or_else(|| anyhow!("hty_id is required"))?;
|
|
let current_org_id = jwt_decode_token(&(*token).clone())?.current_org_id;
|
|
debug!(
|
|
"raw_create_clazz_with_repeat -> id_user: {:?}",
|
|
id_user
|
|
);
|
|
|
|
let new_clazz_id = uuid();
|
|
|
|
let id_sudoer = in_kecheng
|
|
.root_id
|
|
.clone()
|
|
.unwrap_or_else(|| new_clazz_id.clone());
|
|
|
|
let id_parent = in_kecheng
|
|
.parent_id
|
|
.clone()
|
|
.unwrap_or_else(|| new_clazz_id.clone());
|
|
|
|
let mut to_create_kecheng = Clazz {
|
|
id: new_clazz_id.clone(),
|
|
clazz_name: in_kecheng
|
|
.clazz_name
|
|
.clone()
|
|
.ok_or_else(|| anyhow!("clazz_name is required"))?,
|
|
clazz_status: in_kecheng
|
|
.clazz_status
|
|
.clone()
|
|
.ok_or_else(|| anyhow!("clazz_status is required"))?,
|
|
clazz_desc: in_kecheng.clazz_desc.clone(),
|
|
start_from: in_kecheng
|
|
.start_from
|
|
.clone()
|
|
.ok_or_else(|| anyhow!("start_from is required"))?,
|
|
end_by: in_kecheng
|
|
.end_by
|
|
.clone()
|
|
.ok_or_else(|| anyhow!("end_by is required"))?,
|
|
duration: in_kecheng.duration.clone(),
|
|
root_id: id_sudoer.clone(),
|
|
clazz_type: in_kecheng.clazz_type.clone(),
|
|
parent_id: id_parent.clone(),
|
|
created_by: in_kecheng.created_by.clone(),
|
|
created_at: Some(current_local_datetime()),
|
|
students: in_kecheng.students.clone(),
|
|
teachers: in_kecheng.teachers.clone(),
|
|
jihuas: in_kecheng.jihuas.clone(),
|
|
dakas: in_kecheng.dakas.clone(),
|
|
is_delete: in_kecheng.is_delete.clone(),
|
|
is_repeat: in_kecheng.is_repeat.clone(),
|
|
course_sections: in_kecheng.course_sections.clone(),
|
|
is_notified: in_kecheng.is_notified.clone(),
|
|
completed_at: None,
|
|
org_id: in_kecheng.org_id.clone(),
|
|
};
|
|
if to_create_kecheng.org_id.is_none() {
|
|
to_create_kecheng.org_id = current_org_id.clone();
|
|
}
|
|
|
|
debug!(
|
|
"raw_create_clazz_with_repeat -> to_create_kecheng: {:?}",
|
|
to_create_kecheng
|
|
);
|
|
|
|
let mut to_create_kecheng_repeat: Option<ClazzRepeat> = None;
|
|
|
|
if let Some(kecheng_repeat) = &in_kecheng.clazz_repeat {
|
|
let kecheng_repeat_copy = kecheng_repeat.clone();
|
|
to_create_kecheng_repeat = Some(ClazzRepeat {
|
|
id: uuid(),
|
|
clazz_id: Some(new_clazz_id.clone()),
|
|
start_from: kecheng_repeat_copy.start_from.clone(),
|
|
end_by: kecheng_repeat_copy.end_by.clone(),
|
|
repeat_start: kecheng_repeat_copy.repeat_start.clone(),
|
|
repeat_cycle_days: kecheng_repeat_copy.repeat_cycle_days.clone(),
|
|
repeat_end: kecheng_repeat_copy.repeat_end.clone(),
|
|
repeat_status: kecheng_repeat_copy.repeat_status.clone(),
|
|
latest_clazz_created_at: kecheng_repeat_copy.latest_clazz_created_at.clone(),
|
|
org_id: kecheng_repeat_copy.org_id.clone(),
|
|
})
|
|
}
|
|
|
|
let mut params = HashMap::new();
|
|
params.insert(
|
|
"params".to_string(),
|
|
(to_create_kecheng, to_create_kecheng_repeat),
|
|
);
|
|
|
|
let out_result = raw_create_clazz_with_repeat_tx(params, db_pool.clone());
|
|
|
|
match out_result {
|
|
Ok(out) => {
|
|
debug!("created_kecheng: {:?}", out);
|
|
write_clazz_audit_log(
|
|
&db_pool,
|
|
&new_clazz_id,
|
|
"CREATE",
|
|
&id_user,
|
|
Some(serde_json::json!({
|
|
"clazz_name": in_kecheng.clazz_name,
|
|
"start_from": in_kecheng.start_from,
|
|
"end_by": in_kecheng.end_by,
|
|
"is_repeat": in_kecheng.is_repeat,
|
|
})),
|
|
current_org_id.clone(),
|
|
);
|
|
Ok(out)
|
|
}
|
|
Err(e) => Err(anyhow!(HtyErr {
|
|
code: HtyErrCode::WebErr,
|
|
reason: Some("fail to create clazz e: ".to_string() + &e.to_string()),
|
|
})),
|
|
}
|
|
}
|
|
|
|
pub async fn list_clazz_audit_log(
|
|
_sudoer: HtySudoerTokenHeader,
|
|
_host: HtyHostHeader,
|
|
_auth: AuthorizationHeader,
|
|
State(db_pool): State<Arc<DbState>>,
|
|
Query(params): Query<HashMap<String, String>>,
|
|
) -> Json<HtyResponse<Vec<ClazzAuditLog>>> {
|
|
let result = (|| -> anyhow::Result<Vec<ClazzAuditLog>> {
|
|
let clazz_id = get_some_from_query_params::<String>("clazz_id", ¶ms)
|
|
.ok_or_else(|| anyhow!("clazz_id is required"))?;
|
|
ClazzAuditLog::list_by_clazz_id(
|
|
&clazz_id,
|
|
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
|
)
|
|
})();
|
|
match result {
|
|
Ok(ok) => wrap_json_ok_resp(ok),
|
|
Err(e) => {
|
|
error!("list_clazz_audit_log -> failed, e: {}", e);
|
|
wrap_json_anyhow_err(e)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use htycommons::common::current_local_datetime;
|
|
use htycommons::jwt::jwt_encode_token;
|
|
use htycommons::web::{AuthorizationHeader, HtyToken};
|
|
|
|
use super::current_org_id_from_auth;
|
|
|
|
fn build_test_auth_header(current_org_id: Option<&str>) -> String {
|
|
std::env::set_var("JWT_KEY", "htykc-org-test-key");
|
|
let token = HtyToken {
|
|
token_id: "test-token-id".to_string(),
|
|
hty_id: Some("teacher-1".to_string()),
|
|
app_id: Some("app-1".to_string()),
|
|
ts: current_local_datetime(),
|
|
roles: None,
|
|
tags: None,
|
|
current_org_id: current_org_id.map(|value| value.to_string()),
|
|
current_org_role_keys: Some(vec!["TEACHER".to_string()]),
|
|
current_department_id: None,
|
|
};
|
|
jwt_encode_token(token).expect("encode test token")
|
|
}
|
|
|
|
#[test]
|
|
fn should_extract_current_org_id_from_auth_header() {
|
|
let auth_header = build_test_auth_header(Some("org-456"));
|
|
let parsed_org_id = current_org_id_from_auth(&AuthorizationHeader(auth_header));
|
|
assert_eq!(parsed_org_id, Some("org-456".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn should_return_none_when_auth_header_has_no_org_context() {
|
|
let auth_header = build_test_auth_header(None);
|
|
let parsed_org_id = current_org_id_from_auth(&AuthorizationHeader(auth_header));
|
|
assert_eq!(parsed_org_id, None);
|
|
}
|
|
}
|