quote deletion 🎉😮

This commit is contained in:
2026-05-06 00:43:20 +02:00
parent 9eb3332576
commit dd75d89472
8 changed files with 88 additions and 7 deletions

View File

@@ -4,7 +4,7 @@ use strum::{EnumDiscriminants, EnumIter, IntoStaticStr, VariantNames};
use url::Url;
use uuid::Uuid;
use crate::{database::DatabaseError, users::User, web::icons};
use crate::{database::DatabaseError, quotes::Quote, users::User, web::icons};
#[derive(Debug)]
pub struct LogEntry {
@@ -152,6 +152,9 @@ pub enum LogAction {
CreateQuote {
id: Uuid,
},
DeleteQuote {
quote: Quote,
},
ManuallyRevokeSession {
id: Uuid,
},
@@ -179,6 +182,8 @@ impl LogAction {
| Self::DeleteTag { id, .. }
| Self::ManuallyChangeUsersPassword { id } => Some(*id),
Self::DeleteQuote { quote } => Some(quote.id),
Self::AddPersonName { pid, .. }
| Self::DeletePersonName { pid, .. }
| Self::SetPersonPrimaryName { pid, .. } => Some(*pid),
@@ -225,6 +230,9 @@ impl LogAction {
LogAction::CreateQuote { id } => {
format!("Created quote of ID {id}")
}
LogAction::DeleteQuote { quote } => {
format!("Deleted quote of ID {}", quote.id)
}
LogAction::ManuallyRevokeSession { id } => {
format!("Revoked session of ID {id}")
}
@@ -253,6 +261,7 @@ impl LogActionDiscriminant {
LAD::DeletePersonName => "Person Name Deletion",
LAD::SetPersonPrimaryName => "Person Primary Name Set",
LAD::CreateQuote => "Quote Creation",
LAD::DeleteQuote => "Quote Deletion",
LAD::ManuallyRevokeSession => "Manual Session Revocation",
LAD::ChangeInstanceName => "Instance Name Change",
LAD::ChangeDiscordWebhookUrl => "Discord Webhook URL Change",

View File

@@ -2,7 +2,7 @@ use axum::{
http::StatusCode,
response::{IntoResponse, Response},
};
use serde::Serialize;
use serde::{Deserialize, Serialize};
use sqlx::{PgConnection, Row};
use uuid::Uuid;
@@ -14,7 +14,7 @@ pub struct Person {
pub primary_name: String,
}
#[derive(Serialize)]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Name {
pub id: Uuid,
pub is_primary: bool,

View File

@@ -3,7 +3,7 @@ use axum::{
response::{IntoResponse, Response},
};
use chrono::{DateTime, NaiveDateTime, Utc};
use serde::Serialize;
use serde::{Deserialize, Serialize};
use sqlx::{PgConnection, Row};
use uuid::Uuid;
@@ -11,7 +11,7 @@ use crate::{database::DatabaseError, persons::Name};
mod webhook;
#[derive(Serialize)]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Quote {
pub id: Uuid,
pub lines: Vec<QuoteLine>,
@@ -22,7 +22,7 @@ pub struct Quote {
pub public: bool,
}
#[derive(Serialize)]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct QuoteLine {
pub id: Uuid,
pub attribution: Vec<Name>,
@@ -290,6 +290,40 @@ impl Quote {
public,
})
}
pub async fn delete(self, conn: &mut PgConnection) -> Result<(), QuoteError> {
sqlx::query(
r#"
DELETE FROM line_authors
WHERE line_id IN (SELECT id FROM lines WHERE quote_id = $1)
"#,
)
.bind(self.id)
.execute(&mut *conn)
.await?;
sqlx::query("DELETE FROM lines WHERE quote_id = $1")
.bind(self.id)
.execute(&mut *conn)
.await?;
sqlx::query("DELETE FROM quote_tags WHERE quote_id = $1")
.bind(self.id)
.execute(&mut *conn)
.await?;
sqlx::query("DELETE FROM user_quote_likes WHERE quote_id = $1")
.bind(self.id)
.execute(&mut *conn)
.await?;
sqlx::query("DELETE FROM quotes WHERE id = $1")
.bind(self.id)
.execute(&mut *conn)
.await?;
Ok(())
}
}
impl From<sqlx::Error> for QuoteError {

View File

@@ -16,6 +16,8 @@ pub enum Permission {
CreateTags,
RenameTags,
DeleteTags,
#[allow(unused)]
DeleteQuotes,
ChangePersonPrimaryName,
#[allow(unused)]
BrowseServerLogs,

View File

@@ -26,6 +26,7 @@ pub const SERVER: &str = include_str!("server.svg");
pub const SETTINGS: &str = include_str!("settings.svg");
pub const SHIELD_USER: &str = include_str!("shield-user.svg");
pub const TAG: &str = include_str!("tag.svg");
pub const TRASH: &str = include_str!("trash.svg");
pub const TYPE: &str = include_str!("type.svg");
pub const USER: &str = include_str!("user.svg");
pub const USER_KEY: &str = include_str!("user-key.svg");

1
src/web/icons/trash.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash-icon lucide-trash"><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>

After

Width:  |  Height:  |  Size: 355 B

View File

@@ -51,6 +51,7 @@ pub fn pages() -> Router<MnemoState> {
//
.route("/quotes", get(quotes::page))
.route("/quotes/{id}", get(quotes::id::page))
.route("/quotes/{id}/delete", post(quotes::id::delete))
.route("/quotes/add", get(quotes::add::page))
.route("/quotes/add-form", post(quotes::add::form))
//

View File

@@ -1,5 +1,6 @@
use axum::{
extract::{Path, Request, State},
http::HeaderMap,
response::{IntoResponse, Redirect, Response},
};
use maud::{PreEscaped, html};
@@ -8,8 +9,12 @@ use uuid::Uuid;
use crate::{
MnemoState,
error::CompositeError,
logs::{LogAction, LogEntry},
quotes::Quote,
users::{User, auth::UserAuthenticate},
users::{
User,
auth::{UserAuthRequired, UserAuthenticate},
},
web::{
components::{nav::nav, quote::quote},
icons,
@@ -43,6 +48,18 @@ pub async fn page(
}
@if let Ok(q) = q {
(quote(&q))
div class="flex flex-row w-full flex-wrap justify-end gap-2 mt-2" {
a href="#" disabled class="opacity-[.5] cursor-not-allowed px-2 py-1 border rounded flex flex-row gap-1 bg-neutral-200/5 border-neutral-200/25 hover:bg-neutral-200/15 hover:border-neutral-200/45" {
span class="scale-[.75]" {(PreEscaped(icons::PEN))}
"Edit"
}
form method="post" action=(format!("/quotes/{id}/delete")) {
button type="submit" class="px-2 py-1 border rounded flex flex-row gap-1 bg-pink-400/10 border-pink-400/25 hover:bg-pink-400/20 hover:border-pink-400/45" {
span class="scale-[.75]" {(PreEscaped(icons::TRASH))}
"Delete"
}
}
}
} @else {
"Failed to fetch quote. Are you sure it exists?"
}
@@ -51,3 +68,19 @@ pub async fn page(
)
.into_response())
}
pub async fn delete(
State(state): State<MnemoState>,
Path(id): Path<Uuid>,
headers: HeaderMap,
) -> Result<Response, CompositeError> {
let mut tx = state.pool.begin().await?;
let u = User::authenticate(&mut *tx, &headers).await?.required()?;
let q = Quote::get_by_id(&mut *tx, id).await?;
LogEntry::new(&mut *tx, u, LogAction::DeleteQuote { quote: q.clone() }).await?;
q.delete(&mut tx).await?;
tx.commit().await?;
Ok(Redirect::to("/quotes").into_response())
}