graceful shutdown
This commit is contained in:
		@@ -2,14 +2,31 @@ mod sender;
 | 
			
		||||
 | 
			
		||||
use std::env;
 | 
			
		||||
 | 
			
		||||
use log::info;
 | 
			
		||||
use log::{info, warn};
 | 
			
		||||
use serenity::client::Context;
 | 
			
		||||
use sqlx::{Executor, MySql};
 | 
			
		||||
use tokio::time::{sleep_until, Duration, Instant};
 | 
			
		||||
use tokio::{
 | 
			
		||||
    sync::broadcast::Receiver,
 | 
			
		||||
    time::{sleep_until, Duration, Instant},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type Database = MySql;
 | 
			
		||||
 | 
			
		||||
pub async fn initialize(ctx: Context, pool: impl Executor<'_, Database = Database> + Copy) {
 | 
			
		||||
pub async fn initialize(
 | 
			
		||||
    mut kill: Receiver<()>,
 | 
			
		||||
    ctx: Context,
 | 
			
		||||
    pool: impl Executor<'_, Database = Database> + Copy,
 | 
			
		||||
) -> Result<(), &'static str> {
 | 
			
		||||
    tokio::select! {
 | 
			
		||||
        output = _initialize(ctx, pool) => Ok(output),
 | 
			
		||||
        _ = kill.recv() => {
 | 
			
		||||
            warn!("Received terminate signal. Goodbye");
 | 
			
		||||
            Err("Received terminate signal. Goodbye")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn _initialize(ctx: Context, pool: impl Executor<'_, Database = Database> + Copy) {
 | 
			
		||||
    let remind_interval = env::var("REMIND_INTERVAL")
 | 
			
		||||
        .map(|inner| inner.parse::<u64>().ok())
 | 
			
		||||
        .ok()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
use std::{collections::HashMap, env, sync::atomic::Ordering};
 | 
			
		||||
 | 
			
		||||
use log::{info, warn};
 | 
			
		||||
use log::{error, info, warn};
 | 
			
		||||
use poise::{
 | 
			
		||||
    serenity::{model::interactions::Interaction, utils::shard_id},
 | 
			
		||||
    serenity_prelude as serenity,
 | 
			
		||||
@@ -15,10 +15,12 @@ pub async fn listener(
 | 
			
		||||
) -> Result<(), Error> {
 | 
			
		||||
    match event {
 | 
			
		||||
        poise::Event::CacheReady { .. } => {
 | 
			
		||||
            info!("Cache Ready!");
 | 
			
		||||
            info!("Preparing to send reminders");
 | 
			
		||||
            info!("Cache Ready! Preparing extra processes");
 | 
			
		||||
 | 
			
		||||
            if !data.is_loop_running.load(Ordering::Relaxed) {
 | 
			
		||||
                let kill_tx = data.broadcast.clone();
 | 
			
		||||
                let kill_recv = data.broadcast.subscribe();
 | 
			
		||||
 | 
			
		||||
                let ctx1 = ctx.clone();
 | 
			
		||||
                let ctx2 = ctx.clone();
 | 
			
		||||
 | 
			
		||||
@@ -29,7 +31,12 @@ pub async fn listener(
 | 
			
		||||
 | 
			
		||||
                if !run_settings.contains("postman") {
 | 
			
		||||
                    tokio::spawn(async move {
 | 
			
		||||
                        postman::initialize(ctx1, &pool1).await;
 | 
			
		||||
                        match postman::initialize(kill_recv, ctx1, &pool1).await {
 | 
			
		||||
                            Ok(_) => {}
 | 
			
		||||
                            Err(e) => {
 | 
			
		||||
                                error!("postman exiting: {}", e);
 | 
			
		||||
                            }
 | 
			
		||||
                        };
 | 
			
		||||
                    });
 | 
			
		||||
                } else {
 | 
			
		||||
                    warn!("Not running postman")
 | 
			
		||||
@@ -37,7 +44,7 @@ pub async fn listener(
 | 
			
		||||
 | 
			
		||||
                if !run_settings.contains("web") {
 | 
			
		||||
                    tokio::spawn(async move {
 | 
			
		||||
                        reminder_web::initialize(ctx2, pool2).await.unwrap();
 | 
			
		||||
                        reminder_web::initialize(kill_tx, ctx2, pool2).await.unwrap();
 | 
			
		||||
                    });
 | 
			
		||||
                } else {
 | 
			
		||||
                    warn!("Not running web")
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										39
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										39
									
								
								src/main.rs
									
									
									
									
									
								
							@@ -12,7 +12,13 @@ mod models;
 | 
			
		||||
mod time_parser;
 | 
			
		||||
mod utils;
 | 
			
		||||
 | 
			
		||||
use std::{collections::HashMap, env, fmt::Formatter, sync::atomic::AtomicBool};
 | 
			
		||||
use std::{
 | 
			
		||||
    collections::HashMap,
 | 
			
		||||
    env,
 | 
			
		||||
    error::Error as StdError,
 | 
			
		||||
    fmt::{Debug, Display, Formatter},
 | 
			
		||||
    sync::atomic::AtomicBool,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use chrono_tz::Tz;
 | 
			
		||||
use dotenv::dotenv;
 | 
			
		||||
@@ -21,7 +27,7 @@ use poise::serenity::model::{
 | 
			
		||||
    id::{GuildId, UserId},
 | 
			
		||||
};
 | 
			
		||||
use sqlx::{MySql, Pool};
 | 
			
		||||
use tokio::sync::RwLock;
 | 
			
		||||
use tokio::sync::{broadcast, broadcast::Sender, RwLock};
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    commands::{info_cmds, moderation_cmds, reminder_cmds, todo_cmds},
 | 
			
		||||
@@ -43,6 +49,7 @@ pub struct Data {
 | 
			
		||||
    recording_macros: RwLock<HashMap<(GuildId, UserId), CommandMacro<Data, Error>>>,
 | 
			
		||||
    popular_timezones: Vec<Tz>,
 | 
			
		||||
    is_loop_running: AtomicBool,
 | 
			
		||||
    broadcast: Sender<()>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl std::fmt::Debug for Data {
 | 
			
		||||
@@ -51,8 +58,33 @@ impl std::fmt::Debug for Data {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct Ended;
 | 
			
		||||
 | 
			
		||||
impl Debug for Ended {
 | 
			
		||||
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
 | 
			
		||||
        f.write_str("Process ended.")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Display for Ended {
 | 
			
		||||
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
 | 
			
		||||
        f.write_str("Process ended.")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl StdError for Ended {}
 | 
			
		||||
 | 
			
		||||
#[tokio::main]
 | 
			
		||||
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
 | 
			
		||||
async fn main() -> Result<(), Box<dyn StdError + Send + Sync>> {
 | 
			
		||||
    let (tx, mut rx) = broadcast::channel(16);
 | 
			
		||||
 | 
			
		||||
    tokio::select! {
 | 
			
		||||
        output = _main(tx) => output,
 | 
			
		||||
        _ = rx.recv() => Err(Box::new(Ended) as Box<dyn StdError + Send + Sync>)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> {
 | 
			
		||||
    env_logger::init();
 | 
			
		||||
 | 
			
		||||
    dotenv()?;
 | 
			
		||||
@@ -157,6 +189,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
 | 
			
		||||
                    popular_timezones,
 | 
			
		||||
                    recording_macros: Default::default(),
 | 
			
		||||
                    is_loop_running: AtomicBool::new(false),
 | 
			
		||||
                    broadcast: tx,
 | 
			
		||||
                })
 | 
			
		||||
            })
 | 
			
		||||
        })
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ mod routes;
 | 
			
		||||
use std::{collections::HashMap, env};
 | 
			
		||||
 | 
			
		||||
use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl};
 | 
			
		||||
use rocket::fs::FileServer;
 | 
			
		||||
use rocket::{fs::FileServer, tokio::sync::broadcast::Sender};
 | 
			
		||||
use rocket_dyn_templates::Template;
 | 
			
		||||
use serenity::{
 | 
			
		||||
    client::Context,
 | 
			
		||||
@@ -53,6 +53,7 @@ async fn internal_server_error() -> Template {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub async fn initialize(
 | 
			
		||||
    kill_channel: Sender<()>,
 | 
			
		||||
    serenity_context: Context,
 | 
			
		||||
    db_pool: Pool<Database>,
 | 
			
		||||
) -> Result<(), Box<dyn std::error::Error>> {
 | 
			
		||||
@@ -119,6 +120,10 @@ pub async fn initialize(
 | 
			
		||||
        .launch()
 | 
			
		||||
        .await?;
 | 
			
		||||
 | 
			
		||||
    warn!("Exiting rocket runtime");
 | 
			
		||||
    // distribute kill signal
 | 
			
		||||
    kill_channel.send(());
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -57,6 +57,14 @@ button.change-color {
 | 
			
		||||
    left: calc(-1rem - 40px);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
button.disable-enable[data-action="enable"]:after {
 | 
			
		||||
    content: "Enable";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
button.disable-enable[data-action="disable"]:after {
 | 
			
		||||
    content: "Disable";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.media-content {
 | 
			
		||||
    overflow-x: visible;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,10 +3,7 @@ let $discordFrame;
 | 
			
		||||
const $loader = document.querySelector("#loader");
 | 
			
		||||
const $colorPickerModal = document.querySelector("div#pickColorModal");
 | 
			
		||||
const $colorPickerInput = $colorPickerModal.querySelector("input");
 | 
			
		||||
 | 
			
		||||
let timezone = luxon.DateTime.now().zone.name;
 | 
			
		||||
const browserTimezone = luxon.DateTime.now().zone.name;
 | 
			
		||||
let botTimezone = "UTC";
 | 
			
		||||
const $deleteReminderBtn = document.querySelector("#delete-reminder-confirm");
 | 
			
		||||
 | 
			
		||||
let channels;
 | 
			
		||||
let roles;
 | 
			
		||||
@@ -75,6 +72,8 @@ function fetch_roles(guild_id) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function fetch_reminders(guild_id) {
 | 
			
		||||
    document.dispatchEvent(new Event("remindersLoading"));
 | 
			
		||||
 | 
			
		||||
    const $reminderBox = document.querySelector("div#guildReminders");
 | 
			
		||||
 | 
			
		||||
    // reset div contents
 | 
			
		||||
@@ -113,7 +112,6 @@ async function fetch_reminders(guild_id) {
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    let $enableBtn = newFrame.querySelector(".disable-enable");
 | 
			
		||||
                    $enableBtn.textContent = reminder["enabled"] ? "Disable" : "Enable";
 | 
			
		||||
                    $enableBtn.dataset.action = reminder["enabled"]
 | 
			
		||||
                        ? "disable"
 | 
			
		||||
                        : "enable";
 | 
			
		||||
@@ -164,14 +162,36 @@ document.addEventListener("remindersLoaded", (event) => {
 | 
			
		||||
                    if (data.error) {
 | 
			
		||||
                        show_error(data.error);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        enableBtn.textContent = data["enabled"] ? "Disable" : "Enable";
 | 
			
		||||
                        enableBtn.dataset.action = data["enabled"] ? "enable" : "disable";
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        reminder.node
 | 
			
		||||
            .querySelector("button.delete-reminder")
 | 
			
		||||
            .addEventListener("click", () => {
 | 
			
		||||
                let uid = reminder.node.closest(".reminderContent").dataset.uid;
 | 
			
		||||
 | 
			
		||||
                $deleteReminderBtn.dataset["uid"] = uid;
 | 
			
		||||
                $deleteReminderBtn.closest(".modal").classList.toggle("is-active");
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
$deleteReminderBtn.addEventListener("click", () => {
 | 
			
		||||
    let guild = document.querySelector(".guildList a.is-active").dataset["guild"];
 | 
			
		||||
 | 
			
		||||
    fetch(`/dashboard/api/guild/${guild}/reminders`, {
 | 
			
		||||
        method: "DELETE",
 | 
			
		||||
        body: JSON.stringify({
 | 
			
		||||
            uid: $deleteReminderBtn.dataset["uid"],
 | 
			
		||||
        }),
 | 
			
		||||
    }).then(() => {
 | 
			
		||||
        document.querySelector("#deleteReminderModal").classList.remove("is-active");
 | 
			
		||||
        fetch_reminders(guild);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function show_error(error) {
 | 
			
		||||
    document.getElementById("errors").querySelector("span.error-message").textContent =
 | 
			
		||||
        error;
 | 
			
		||||
@@ -182,60 +202,6 @@ function show_error(error) {
 | 
			
		||||
    }, 5000);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function update_times() {
 | 
			
		||||
    document.querySelectorAll("span.set-timezone").forEach((element) => {
 | 
			
		||||
        element.textContent = timezone;
 | 
			
		||||
    });
 | 
			
		||||
    document.querySelectorAll("span.set-time").forEach((element) => {
 | 
			
		||||
        element.textContent = luxon.DateTime.now().setZone(timezone).toFormat("HH:mm");
 | 
			
		||||
    });
 | 
			
		||||
    document.querySelectorAll("span.browser-timezone").forEach((element) => {
 | 
			
		||||
        element.textContent = browserTimezone;
 | 
			
		||||
    });
 | 
			
		||||
    document.querySelectorAll("span.browser-time").forEach((element) => {
 | 
			
		||||
        element.textContent = luxon.DateTime.now().toFormat("HH:mm");
 | 
			
		||||
    });
 | 
			
		||||
    document.querySelectorAll("span.bot-timezone").forEach((element) => {
 | 
			
		||||
        element.textContent = botTimezone;
 | 
			
		||||
    });
 | 
			
		||||
    document.querySelectorAll("span.bot-time").forEach((element) => {
 | 
			
		||||
        element.textContent = luxon.DateTime.now().setZone(botTimezone).toFormat("HH:mm");
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
window.setInterval(() => {
 | 
			
		||||
    update_times();
 | 
			
		||||
}, 30000);
 | 
			
		||||
 | 
			
		||||
document.getElementById("set-bot-timezone").addEventListener("click", () => {
 | 
			
		||||
    timezone = botTimezone;
 | 
			
		||||
    update_times();
 | 
			
		||||
});
 | 
			
		||||
document.getElementById("set-browser-timezone").addEventListener("click", () => {
 | 
			
		||||
    timezone = browserTimezone;
 | 
			
		||||
    update_times();
 | 
			
		||||
});
 | 
			
		||||
document.getElementById("update-bot-timezone").addEventListener("click", () => {
 | 
			
		||||
    timezone = browserTimezone;
 | 
			
		||||
    fetch("/dashboard/api/user", {
 | 
			
		||||
        method: "PATCH",
 | 
			
		||||
        headers: {
 | 
			
		||||
            Accept: "application/json",
 | 
			
		||||
            "Content-Type": "application/json",
 | 
			
		||||
        },
 | 
			
		||||
        body: JSON.stringify({ timezone: timezone }),
 | 
			
		||||
    })
 | 
			
		||||
        .then((response) => response.json())
 | 
			
		||||
        .then((data) => {
 | 
			
		||||
            if (data.error) {
 | 
			
		||||
                show_error(data.error);
 | 
			
		||||
            } else {
 | 
			
		||||
                botTimezone = browserTimezone;
 | 
			
		||||
                update_times();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
$colorPickerInput.value = colorPicker.color.hexString;
 | 
			
		||||
 | 
			
		||||
$colorPickerInput.addEventListener("input", () => {
 | 
			
		||||
@@ -482,6 +448,7 @@ $createReminder.querySelector("button#createReminder").addEventListener("click",
 | 
			
		||||
        .then((data) => console.log(data));
 | 
			
		||||
 | 
			
		||||
    // process response
 | 
			
		||||
    fetch_reminders(guild);
 | 
			
		||||
 | 
			
		||||
    // reset inputs
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										57
									
								
								web/static/js/timezone.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								web/static/js/timezone.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,57 @@
 | 
			
		||||
let timezone = luxon.DateTime.now().zone.name;
 | 
			
		||||
const browserTimezone = luxon.DateTime.now().zone.name;
 | 
			
		||||
let botTimezone = "UTC";
 | 
			
		||||
 | 
			
		||||
function update_times() {
 | 
			
		||||
    document.querySelectorAll("span.set-timezone").forEach((element) => {
 | 
			
		||||
        element.textContent = timezone;
 | 
			
		||||
    });
 | 
			
		||||
    document.querySelectorAll("span.set-time").forEach((element) => {
 | 
			
		||||
        element.textContent = luxon.DateTime.now().setZone(timezone).toFormat("HH:mm");
 | 
			
		||||
    });
 | 
			
		||||
    document.querySelectorAll("span.browser-timezone").forEach((element) => {
 | 
			
		||||
        element.textContent = browserTimezone;
 | 
			
		||||
    });
 | 
			
		||||
    document.querySelectorAll("span.browser-time").forEach((element) => {
 | 
			
		||||
        element.textContent = luxon.DateTime.now().toFormat("HH:mm");
 | 
			
		||||
    });
 | 
			
		||||
    document.querySelectorAll("span.bot-timezone").forEach((element) => {
 | 
			
		||||
        element.textContent = botTimezone;
 | 
			
		||||
    });
 | 
			
		||||
    document.querySelectorAll("span.bot-time").forEach((element) => {
 | 
			
		||||
        element.textContent = luxon.DateTime.now().setZone(botTimezone).toFormat("HH:mm");
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
window.setInterval(() => {
 | 
			
		||||
    update_times();
 | 
			
		||||
}, 30000);
 | 
			
		||||
 | 
			
		||||
document.getElementById("set-bot-timezone").addEventListener("click", () => {
 | 
			
		||||
    timezone = botTimezone;
 | 
			
		||||
    update_times();
 | 
			
		||||
});
 | 
			
		||||
document.getElementById("set-browser-timezone").addEventListener("click", () => {
 | 
			
		||||
    timezone = browserTimezone;
 | 
			
		||||
    update_times();
 | 
			
		||||
});
 | 
			
		||||
document.getElementById("update-bot-timezone").addEventListener("click", () => {
 | 
			
		||||
    timezone = browserTimezone;
 | 
			
		||||
    fetch("/dashboard/api/user", {
 | 
			
		||||
        method: "PATCH",
 | 
			
		||||
        headers: {
 | 
			
		||||
            Accept: "application/json",
 | 
			
		||||
            "Content-Type": "application/json",
 | 
			
		||||
        },
 | 
			
		||||
        body: JSON.stringify({ timezone: timezone }),
 | 
			
		||||
    })
 | 
			
		||||
        .then((response) => response.json())
 | 
			
		||||
        .then((data) => {
 | 
			
		||||
            if (data.error) {
 | 
			
		||||
                show_error(data.error);
 | 
			
		||||
            } else {
 | 
			
		||||
                botTimezone = browserTimezone;
 | 
			
		||||
                update_times();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
});
 | 
			
		||||
@@ -140,6 +140,27 @@
 | 
			
		||||
    <button class="modal-close is-large close-modal" aria-label="close"></button>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div class="modal" id="deleteReminderModal">
 | 
			
		||||
    <div class="modal-background"></div>
 | 
			
		||||
    <div class="modal-card">
 | 
			
		||||
        <header class="modal-card-head">
 | 
			
		||||
            <label class="modal-card-title" for="urlInput">Delete Reminder</label>
 | 
			
		||||
            <button class="delete close-modal" aria-label="close"></button>
 | 
			
		||||
        </header>
 | 
			
		||||
        <section class="modal-card-body">
 | 
			
		||||
            <p>
 | 
			
		||||
                This reminder will be permenantly deleted. Are you sure?
 | 
			
		||||
            </p>
 | 
			
		||||
            <br>
 | 
			
		||||
            <div class="has-text-centered">
 | 
			
		||||
                <button class="button is-danger" id="delete-reminder-confirm">Delete</button>
 | 
			
		||||
                <button class="button is-light close-modal">Cancel</button>
 | 
			
		||||
            </div>
 | 
			
		||||
        </section>
 | 
			
		||||
    </div>
 | 
			
		||||
    <button class="modal-close is-large close-modal" aria-label="close"></button>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div class="columns is-gapless dashboard-frame">
 | 
			
		||||
    <div class="column is-2 is-sidebar-menu dashboard-sidebar is-hidden-touch" style="display: flex; flex-direction: column;">
 | 
			
		||||
        <a href="/">
 | 
			
		||||
@@ -300,6 +321,7 @@
 | 
			
		||||
    {% include "reminder_dashboard/personal_reminder" %}
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script src="/static/js/timezone.js"></script>
 | 
			
		||||
<script src="/static/js/main.js"></script>
 | 
			
		||||
 | 
			
		||||
</body>
 | 
			
		||||
 
 | 
			
		||||
@@ -215,9 +215,8 @@
 | 
			
		||||
                    Saved!
 | 
			
		||||
                </button>
 | 
			
		||||
                <button class="button is-warning disable-enable">
 | 
			
		||||
                    Disable
 | 
			
		||||
                </button>
 | 
			
		||||
                <button class="button is-danger">
 | 
			
		||||
                <button class="button is-danger delete-reminder">
 | 
			
		||||
                    Delete
 | 
			
		||||
                </button>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user