feat: displaying and managing active jobs

This commit is contained in:
Per Stark
2025-01-28 11:51:45 +01:00
parent ae63416943
commit 0fe253a127
13 changed files with 522 additions and 181 deletions

View File

@@ -31,9 +31,8 @@ use zettle_db::{
admin_panel::{show_admin_panel, toggle_registration_status},
documentation::index::show_documentation_index,
gdpr::{accept_gdpr, deny_gdpr},
index::index_handler,
ingress_form::{process_ingress_form, show_ingress_form},
ingress_tasks::{delete_task, show_queue_tasks},
index::{delete_job, delete_text_content, index_handler},
ingress_form::{hide_ingress_form, process_ingress_form, show_ingress_form},
privacy_policy::show_privacy_policy,
search_result::search_result_handler,
signin::{authenticate_user, show_signin_form},
@@ -168,8 +167,9 @@ fn html_routes(
"/ingress-form",
get(show_ingress_form).post(process_ingress_form),
)
.route("/queue", get(show_queue_tasks))
.route("/queue/:delivery_tag", delete(delete_task))
.route("/hide-ingress-form", get(hide_ingress_form))
.route("/text-content/:id", delete(delete_text_content))
.route("/jobs/:job_id", delete(delete_job))
.route("/account", get(show_account_page))
.route("/admin", get(show_admin_panel))
.route("/toggle-registrations", patch(toggle_registration_status))

View File

@@ -57,6 +57,31 @@ impl JobQueue {
Ok(jobs)
}
/// Gets all active jobs for a specific user
pub async fn get_unfinished_user_jobs(&self, user_id: &str) -> Result<Vec<Job>, AppError> {
let jobs: Vec<Job> = self
.db
.query(
"SELECT * FROM type::table($table)
WHERE user_id = $user_id
AND (
status = 'Created'
OR (
status.InProgress != NONE
AND status.InProgress.attempts < $max_attempts
)
)
ORDER BY created_at DESC",
)
.bind(("table", Job::table_name()))
.bind(("user_id", user_id.to_owned()))
.bind(("max_attempts", MAX_ATTEMPTS))
.await?
.take(0)?;
debug!("{:?}", jobs);
Ok(jobs)
}
pub async fn delete_job(&self, id: &str, user_id: &str) -> Result<(), AppError> {
get_item::<Job>(&self.db.client, id)
.await?

View File

@@ -1,4 +1,7 @@
use axum::{extract::State, response::IntoResponse};
use axum::{
extract::{Path, State},
response::{IntoResponse, Redirect},
};
use axum_session::Session;
use axum_session_auth::AuthSession;
use axum_session_surreal::SessionSurrealPool;
@@ -6,17 +9,23 @@ use surrealdb::{engine::any::Any, Surreal};
use tracing::info;
use crate::{
error::HtmlError,
error::{AppError, HtmlError},
page_data,
server::{routes::html::render_template, AppState},
storage::types::{text_content::TextContent, user::User},
server::{
routes::html::{render_block, render_template},
AppState,
},
storage::{
db::delete_item,
types::{job::Job, text_content::TextContent, user::User},
},
};
page_data!(IndexData, "index/index.html", {
gdpr_accepted: bool,
queue_length: u32,
user: Option<User>,
latest_text_contents: Vec<TextContent>
latest_text_contents: Vec<TextContent>,
active_jobs: Vec<Job>
});
pub async fn index_handler(
@@ -28,17 +37,16 @@ pub async fn index_handler(
let gdpr_accepted = auth.current_user.is_some() | session.get("gdpr_accepted").unwrap_or(false);
let queue_length = match auth.current_user.is_some() {
let active_jobs = match auth.current_user.is_some() {
true => state
.job_queue
.get_user_jobs(&auth.current_user.clone().unwrap().id)
.get_unfinished_user_jobs(&auth.current_user.clone().unwrap().id)
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?
.len(),
false => 0,
.map_err(|e| HtmlError::new(e, state.templates.clone()))?,
false => vec![],
};
let latest_text_contents = match auth.current_user.is_some() {
let latest_text_contents = match auth.current_user.clone().is_some() {
true => User::get_latest_text_contents(
auth.current_user.clone().unwrap().id.as_str(),
&state.surreal_db_client,
@@ -65,10 +73,90 @@ pub async fn index_handler(
let output = render_template(
IndexData::template_name(),
IndexData {
queue_length: queue_length.try_into().unwrap(),
gdpr_accepted,
user: auth.current_user,
latest_text_contents,
active_jobs,
},
state.templates.clone(),
)?;
Ok(output.into_response())
}
#[derive(Serialize)]
pub struct LatestTextContentData {
latest_text_contents: Vec<TextContent>,
user: User,
}
pub async fn delete_text_content(
State(state): State<AppState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
Path(id): Path<String>,
) -> Result<impl IntoResponse, HtmlError> {
let user = match &auth.current_user {
Some(user) => user,
None => return Ok(Redirect::to("/").into_response()),
};
delete_item::<TextContent>(&state.surreal_db_client, &id)
.await
.map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?;
let latest_text_contents = User::get_latest_text_contents(&user.id, &state.surreal_db_client)
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
info!("{:?}", latest_text_contents);
let output = render_block(
"index/signed_in/recent_content.html",
"latest_content_section",
LatestTextContentData {
user: user.clone(),
latest_text_contents,
},
state.templates.clone(),
)?;
Ok(output.into_response())
}
#[derive(Serialize)]
pub struct ActiveJobsData {
active_jobs: Vec<Job>,
user: User,
}
pub async fn delete_job(
State(state): State<AppState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
Path(id): Path<String>,
) -> Result<impl IntoResponse, HtmlError> {
let user = match auth.current_user {
Some(user) => user,
None => return Ok(Redirect::to("/signin").into_response()),
};
state
.job_queue
.delete_job(&id, &user.id)
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
let active_jobs = state
.job_queue
.get_unfinished_user_jobs(&user.id)
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
let output = render_block(
"index/signed_in/active_jobs.html",
"active_jobs_section",
ActiveJobsData {
user: user.clone(),
active_jobs,
},
state.templates.clone(),
)?;

View File

@@ -33,6 +33,19 @@ pub async fn show_ingress_form(
Ok(output.into_response())
}
pub async fn hide_ingress_form(
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
) -> Result<impl IntoResponse, HtmlError> {
if !auth.is_authenticated() {
return Ok(Redirect::to("/").into_response());
}
Ok(Html(
"<a class='btn btn-primary' hx-get='/ingress-form' hx-swap='outerHTML'>Add Content</a>",
)
.into_response())
}
#[derive(Debug, TryFromMultipart)]
pub struct IngressParams {
pub content: Option<String>,

View File

@@ -1,60 +0,0 @@
use crate::{
error::HtmlError,
page_data,
server::AppState,
storage::types::{job::Job, user::User},
};
use axum::{
extract::{Path, State},
response::{Html, IntoResponse, Redirect},
};
use axum_session_auth::AuthSession;
use axum_session_surreal::SessionSurrealPool;
use surrealdb::{engine::any::Any, Surreal};
use super::render_template;
page_data!(ShowQueueTasks, "queue_tasks.html", {user : User,jobs: Vec<Job>});
pub async fn show_queue_tasks(
State(state): State<AppState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
) -> Result<impl IntoResponse, HtmlError> {
let user = match auth.current_user {
Some(user) => user,
None => return Ok(Redirect::to("/signin").into_response()),
};
let jobs = state
.job_queue
.get_user_jobs(&user.id)
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
let rendered = render_template(
ShowQueueTasks::template_name(),
ShowQueueTasks { jobs, user },
state.templates.clone(),
)?;
Ok(rendered.into_response())
}
pub async fn delete_task(
State(state): State<AppState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
Path(id): Path<String>,
) -> Result<impl IntoResponse, HtmlError> {
let user = match auth.current_user {
Some(user) => user,
None => return Ok(Redirect::to("/signin").into_response()),
};
state
.job_queue
.delete_job(&id, &user.id)
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
Ok(Html("").into_response())
}

View File

@@ -11,7 +11,6 @@ pub mod documentation;
pub mod gdpr;
pub mod index;
pub mod ingress_form;
pub mod ingress_tasks;
pub mod privacy_policy;
pub mod search_result;
pub mod signin;