jude/orphan-reminders #1

Merged
jude merged 10 commits from jude/orphan-reminders into next 2023-09-16 17:09:34 +00:00
15 changed files with 172 additions and 85 deletions
Showing only changes of commit ee89cb40c5 - Show all commits

View File

@ -0,0 +1,4 @@
ALTER TABLE reminders ADD COLUMN `status_change_time` DATETIME;
-- This is a best-guess as to the status change time.
UPDATE reminders SET `status_change_time` = `utc_time`;

View File

@ -166,15 +166,21 @@ impl ComponentDataModel {
.await; .await;
} }
ComponentDataModel::DelSelector(selector) => { ComponentDataModel::DelSelector(selector) => {
let selected_id = component.data.values.join(","); for id in &component.data.values {
match id.parse::<u32>() {
Ok(id) => {
if let Some(reminder) = Reminder::from_id(&data.database, id).await {
reminder.delete(&data.database).await.unwrap();
} else {
warn!("Attempt to delete non-existent reminder");
}
}
sqlx::query!( Err(e) => {
"UPDATE reminders SET `status` = 'deleted' WHERE FIND_IN_SET(id, ?)", warn!("Error casting ID to integer: {:?}.", e);
selected_id }
) }
.execute(&data.database) }
.await
.unwrap();
let reminders = Reminder::from_guild( let reminders = Reminder::from_guild(
&ctx, &ctx,

View File

@ -304,7 +304,10 @@ WHERE
&self, &self,
db: impl Executor<'_, Database = Database>, db: impl Executor<'_, Database = Database>,
) -> Result<(), sqlx::Error> { ) -> Result<(), sqlx::Error> {
sqlx::query!("UPDATE reminders SET `status` = 'deleted' WHERE uid = ?", self.uid) sqlx::query!(
"UPDATE reminders SET `status` = 'deleted', `status_change_time` = NOW() WHERE uid = ?",
self.uid
)
.execute(db) .execute(db)
.await .await
.map(|_| ()) .map(|_| ())

View File

@ -166,7 +166,6 @@ pub async fn initialize(
routes::dashboard::guild::get_reminders, routes::dashboard::guild::get_reminders,
routes::dashboard::guild::edit_reminder, routes::dashboard::guild::edit_reminder,
routes::dashboard::guild::delete_reminder, routes::dashboard::guild::delete_reminder,
routes::dashboard::guild::get_reminder_errors,
routes::dashboard::export::export_reminders, routes::dashboard::export::export_reminders,
routes::dashboard::export::export_reminder_templates, routes::dashboard::export::export_reminder_templates,
routes::dashboard::export::export_todos, routes::dashboard::export::export_todos,

View File

@ -171,6 +171,8 @@ pub async fn import_reminders(
uid: generate_uid(), uid: generate_uid(),
username: record.username, username: record.username,
utc_time: record.utc_time, utc_time: record.utc_time,
status: "pending".to_string(),
status_change_time: None,
}; };
create_reminder( create_reminder(

View File

@ -26,7 +26,7 @@ use crate::{
routes::{ routes::{
dashboard::{ dashboard::{
create_database_channel, create_reminder, template_name_default, DeleteReminder, create_database_channel, create_reminder, template_name_default, DeleteReminder,
DeleteReminderTemplate, PatchReminder, Reminder, ReminderError, ReminderTemplate, DeleteReminderTemplate, PatchReminder, Reminder, ReminderTemplate,
}, },
JsonResult, JsonResult,
}, },
@ -318,15 +318,18 @@ pub async fn create_guild_reminder(
.await .await
} }
#[get("/api/guild/<id>/reminders")] #[get("/api/guild/<id>/reminders?<status>")]
pub async fn get_reminders( pub async fn get_reminders(
id: u64, id: u64,
cookies: &CookieJar<'_>, cookies: &CookieJar<'_>,
serenity_context: &State<Context>, serenity_context: &State<Context>,
pool: &State<Pool<MySql>>, pool: &State<Pool<MySql>>,
status: Option<String>,
) -> JsonResult { ) -> JsonResult {
check_authorization!(cookies, serenity_context.inner(), id); check_authorization!(cookies, serenity_context.inner(), id);
let status = status.unwrap_or("pending".to_string());
sqlx::query_as_unchecked!( sqlx::query_as_unchecked!(
Reminder, Reminder,
"SELECT "SELECT
@ -355,10 +358,13 @@ pub async fn get_reminders(
reminders.tts, reminders.tts,
reminders.uid, reminders.uid,
reminders.username, reminders.username,
reminders.utc_time reminders.utc_time,
reminders.status,
reminders.status_change_time
FROM reminders FROM reminders
LEFT JOIN channels ON channels.id = reminders.channel_id LEFT JOIN channels ON channels.id = reminders.channel_id
WHERE `status` = 'pending' AND reminders.guild_id = (SELECT id FROM guilds WHERE guild = ?)", WHERE FIND_IN_SET(`status`, ?) AND reminders.guild_id = (SELECT id FROM guilds WHERE guild = ?)",
status,
id id
) )
.fetch_all(pool.inner()) .fetch_all(pool.inner())
@ -567,7 +573,9 @@ pub async fn edit_reminder(
reminders.tts, reminders.tts,
reminders.uid, reminders.uid,
reminders.username, reminders.username,
reminders.utc_time reminders.utc_time,
reminders.status,
reminders.status_change_time
FROM reminders FROM reminders
LEFT JOIN channels ON channels.id = reminders.channel_id LEFT JOIN channels ON channels.id = reminders.channel_id
WHERE uid = ?", WHERE uid = ?",
@ -591,7 +599,10 @@ pub async fn delete_reminder(
reminder: Json<DeleteReminder>, reminder: Json<DeleteReminder>,
pool: &State<Pool<MySql>>, pool: &State<Pool<MySql>>,
) -> JsonResult { ) -> JsonResult {
match sqlx::query!("UPDATE reminders SET `status` = 'deleted' WHERE uid = ?", reminder.uid) match sqlx::query!(
"UPDATE reminders SET `status` = 'deleted', `status_change_time` = NOW() WHERE uid = ?",
reminder.uid
)
.execute(pool.inner()) .execute(pool.inner())
.await .await
{ {
@ -604,35 +615,3 @@ pub async fn delete_reminder(
} }
} }
} }
#[get("/api/guild/<id>/errors")]
pub async fn get_reminder_errors(
id: u64,
cookies: &CookieJar<'_>,
serenity_context: &State<Context>,
pool: &State<Pool<MySql>>,
) -> JsonResult {
check_authorization!(cookies, serenity_context.inner(), id);
sqlx::query_as_unchecked!(
ReminderError,
"SELECT
reminders.status,
reminders.utc_time,
reminders.name,
reminders.uid,
reminders.channel_id AS channel
FROM reminders
LEFT JOIN channels ON channels.id = reminders.channel_id
WHERE (`status` = 'failed' OR reminders.channel_id IS NULL) AND reminders.guild_id = (SELECT id FROM guilds WHERE guild = ?)",
id
)
.fetch_all(pool.inner())
.await
.map(|r| Ok(json!(r)))
.unwrap_or_else(|e| {
warn!("Failed to complete SQL query: {:?}", e);
json_err!("Could not load reminders")
})
}

View File

@ -150,18 +150,8 @@ pub struct Reminder {
uid: String, uid: String,
username: Option<String>, username: Option<String>,
utc_time: NaiveDateTime, utc_time: NaiveDateTime,
}
#[derive(Serialize)]
pub struct ReminderError {
#[serde(with = "string")]
channel: u64,
status: String, status: String,
#[serde(default = "name_default")] status_change_time: Option<NaiveDateTime>,
name: String,
#[serde(default)]
uid: String,
utc_time: NaiveDateTime,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -574,7 +564,9 @@ pub async fn create_reminder(
reminders.tts, reminders.tts,
reminders.uid, reminders.uid,
reminders.username, reminders.username,
reminders.utc_time reminders.utc_time,
reminders.status,
reminders.status_change_time
FROM reminders FROM reminders
LEFT JOIN channels ON channels.id = reminders.channel_id LEFT JOIN channels ON channels.id = reminders.channel_id
WHERE uid = ?", WHERE uid = ?",

View File

@ -688,6 +688,44 @@ li.highlight {
/* END */ /* END */
div.reminderError {
margin: 10px;
padding: 14px;
background-color: #f5f5f5;
border-radius: 8px;
}
div.reminderError .errorHead {
display: flex;
flex-direction: row;
}
div.reminderError .errorIcon {
padding: 8px;
border-radius: 4px;
margin-right: 12px;
}
div.reminderError .errorIcon.deleted {
background-color: #e7e5e4;
}
div.reminderError .errorIcon.success {
background-color: #d9f99d;
}
div.reminderError .errorIcon.errored {
background-color: #fecaca;
}
div.reminderError .errorHead .reminderName {
font-size: 1rem;
display: flex;
flex-direction: column;
justify-content: center;
color: rgb(54, 54, 54);
}
/* other stuff */ /* other stuff */
.half-rem { .half-rem {

View File

@ -57,7 +57,7 @@ function switch_pane(selector) {
el.classList.add("is-hidden"); el.classList.add("is-hidden");
}); });
document.getElementById(selector).classList.remove("is-hidden"); document.querySelector(`*[data-name=${selector}]`).classList.remove("is-hidden");
} }
function update_select(sel) { function update_select(sel) {
@ -735,16 +735,19 @@ document.addEventListener("DOMContentLoaded", async () => {
}); });
} }
const matches = window.location.href.match(/dashboard\/(\d+)/); const matches = window.location.href.match(
/dashboard\/(\d+)(\/)?([a-zA-Z\-]+)?/
);
if (matches) { if (matches) {
let id = matches[1]; let id = matches[1];
let kind = matches[3];
let name = guildNames[id]; let name = guildNames[id];
const event = new CustomEvent("guildSwitched", { const event = new CustomEvent("guildSwitched", {
detail: { detail: {
guild_name: name, guild_name: name,
guild_id: id, guild_id: id,
pane: "guild", pane: kind,
}, },
}); });

View File

@ -1,16 +1,18 @@
function loadErrors() { function loadErrors() {
return fetch(`/dashboard/api/guild/${guildId()}/errors`).then((response) => return fetch(
response.json() `/dashboard/api/guild/${guildId()}/reminders?status=deleted,sent,error`
); ).then((response) => response.json());
} }
document.addEventListener("paneLoad", (ev) => { document.addEventListener("paneLoad", (ev) => {
if (ev.detail.pane !== "reminder-errors") { if (ev.detail.pane !== "errors") {
return; return;
} }
// Load errors // Load errors
loadErrors().then((res) => {}); loadErrors().then((res) => {
console.log(res);
});
$loader.classList.add("is-hidden"); $loader.classList.add("is-hidden");
}); });

View File

@ -332,16 +332,16 @@
<p class="subtitle is-hidden-desktop">Press the <span class="icon"><i class="fal fa-bars"></i></span> to get started</p> <p class="subtitle is-hidden-desktop">Press the <span class="icon"><i class="fal fa-bars"></i></span> to get started</p>
</div> </div>
</section> </section>
<section id="guild" class="is-hidden"> <section data-name="reminders" class="is-hidden">
{% include "reminder_dashboard/reminder_dashboard" %} {% include "reminder_dashboard/reminder_dashboard" %}
</section> </section>
<section id="reminder-errors" class="is-hidden"> <section data-name="errors" class="is-hidden">
{% include "reminder_dashboard/reminder_errors" %} {% include "reminder_dashboard/reminder_errors" %}
</section> </section>
<section id="guild-error" class="is-hidden"> <section data-name="guild-error" class="is-hidden">
{% include "reminder_dashboard/guild_error" %} {% include "reminder_dashboard/guild_error" %}
</section> </section>
<section id="user-error" class="is-hidden"> <section data-name="user-error" class="is-hidden">
{% include "reminder_dashboard/user_error" %} {% include "reminder_dashboard/user_error" %}
</section> </section>
</div> </div>
@ -375,16 +375,16 @@
<template id="guildListEntry"> <template id="guildListEntry">
<li> <li>
<a class="switch-pane" data-pane="guild" data-slug="reminders"> <a class="switch-pane" data-pane="reminders" data-slug="reminders">
<span class="icon"><i class="fas fa-map-pin"></i></span> <span class="guild-name">%guildname%</span> <span class="icon"><i class="fas fa-map-pin"></i></span> <span class="guild-name">%guildname%</span>
</a> </a>
<ul class="guild-submenu"> <ul class="guild-submenu">
<li> <li>
<a class="switch-pane" data-pane="guild" data-slug="reminders"> <a class="switch-pane" data-pane="reminders" data-slug="reminders">
<span class="icon"><i class="fas fa-calendar-alt"></i></span> Reminders <span class="icon"><i class="fas fa-calendar-alt"></i></span> Reminders
</a> </a>
<a class="switch-pane" data-pane="reminder-errors" data-slug="errors"> <a class="switch-pane" data-pane="errors" data-slug="errors">
<span class="icon"><i class="fas fa-exclamation-triangle"></i></span> Errors <span class="icon"><i class="fas fa-file-alt"></i></span> Logs
</a> </a>
</li> </li>
</ul> </ul>
@ -392,11 +392,11 @@
</template> </template>
<template id="guildReminder"> <template id="guildReminder">
{% include "reminder_dashboard/guild_reminder" %} {% include "reminder_dashboard/templates/guild_reminder" %}
</template> </template>
<template id="guildError"> <template id="reminderError">
{% include "reminder_dashboard/templates/reminder_error" %}
</template> </template>
<script src="/static/js/iro.js"></script> <script src="/static/js/iro.js"></script>

View File

@ -2,7 +2,7 @@
<strong>Create Reminder</strong> <strong>Create Reminder</strong>
<div id="reminderCreator"> <div id="reminderCreator">
{% set creating = true %} {% set creating = true %}
{% include "reminder_dashboard/guild_reminder" %} {% include "reminder_dashboard/templates/guild_reminder" %}
{% set creating = false %} {% set creating = false %}
</div> </div>
<br> <br>

View File

@ -1,5 +1,51 @@
<div> <div>
<div class="reminderError">
<div class="errorHead">
<div class="errorIcon deleted">
<span class="icon">
<i class="fas fa-trash"></i>
</span>
</div>
<div class="reminderName">
Reminder
</div>
<div class="reminderTime">
</div>
</div>
</div>
<div class="reminderError">
<div class="errorHead">
<div class="errorIcon errored">
<span class="icon">
<i class="fas fa-exclamation-triangle"></i>
</span>
</div>
<div class="reminderName">
Reminder
</div>
<div class="reminderTime">
</div>
</div>
</div>
<div class="reminderError">
<div class="errorHead">
<div class="errorIcon success">
<span class="icon">
<i class="fas fa-check"></i>
</span>
</div>
<div class="reminderName">
Reminder
</div>
<div class="reminderTime">
</div>
</div>
</div>
</div> </div>
<script src="/static/js/reminder_errors.js"></script> <script src="/static/js/reminder_errors.js"></script>

View File

@ -0,0 +1,13 @@
<div class="reminderError">
<div class="errorIcon">
<span>
<i class="fas fa-trash"></i>
</span>
</div>
<div>
<span class="reminderName"></span>
</div>
<div>
<span></span>
</div>
</div>