Working database submission display
This commit is contained in:
parent
b20fa28198
commit
e2a35a271b
9 changed files with 274 additions and 65 deletions
|
@ -52,8 +52,7 @@ async fn main() {
|
|||
return;
|
||||
}
|
||||
let http = http();
|
||||
Database::new(false)
|
||||
.expect("Failed to load database")
|
||||
load_database()
|
||||
.load_legacy(&http).await
|
||||
.expect("Failed to load legacy submissions");
|
||||
},
|
||||
|
@ -64,6 +63,10 @@ async fn main() {
|
|||
}
|
||||
}
|
||||
|
||||
fn load_database() -> Database {
|
||||
Database::new(false).expect("Failed to load database")
|
||||
}
|
||||
|
||||
fn http() -> Http {
|
||||
let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment");
|
||||
Http::new(&token)
|
||||
|
@ -81,11 +84,13 @@ async fn rocket() -> Result<Rocket<Ignite>, rocket::Error> {
|
|||
rocket::custom(config)
|
||||
.manage(Settings::new(&http).await.unwrap())
|
||||
.manage(http)
|
||||
.manage(load_database())
|
||||
.mount(
|
||||
"/",
|
||||
routes![get_challenge, get_guilds, login, post_login, success, logout, testing],
|
||||
)
|
||||
.mount("/css", FileServer::from(relative!("styles/css")))
|
||||
.mount("/", FileServer::from(relative!("assets")).rank(2))
|
||||
.mount("/", FileServer::from(relative!("static")).rank(1))
|
||||
.attach(Template::custom(move |engines| {
|
||||
use tera::Value;
|
||||
|
|
|
@ -1,18 +1,32 @@
|
|||
use std::collections::HashSet;
|
||||
use std::{fs::File, io::Read, collections::HashMap, path::Path};
|
||||
|
||||
use poise::serenity_prelude::Http;
|
||||
use rusqlite::{Connection, params};
|
||||
use r2d2::{Pool, PooledConnection};
|
||||
use r2d2_sqlite::SqliteConnectionManager;
|
||||
use r2d2_sqlite::rusqlite::{self, params};
|
||||
use derive_more::From;
|
||||
|
||||
use crate::{utils::get_challenge_number, models::User};
|
||||
|
||||
use super::{LegacySubmission, Submission};
|
||||
|
||||
pub struct Database {
|
||||
conn: Connection,
|
||||
// Must be Arc because Connection contains RefCell,
|
||||
// which cannot be shared between threads safely
|
||||
connection_pool: Pool<SqliteConnectionManager>,
|
||||
}
|
||||
|
||||
const DATABASE_FILENAME: &str = "database.db";
|
||||
|
||||
#[derive(From, Debug)]
|
||||
pub enum DatabaseError {
|
||||
Rusqlite(rusqlite::Error),
|
||||
Pool(r2d2::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, DatabaseError>;
|
||||
|
||||
impl Database {
|
||||
pub fn file_exists() -> bool {
|
||||
Path::new(DATABASE_FILENAME).exists()
|
||||
|
@ -20,12 +34,14 @@ impl Database {
|
|||
|
||||
pub fn new(
|
||||
testing: bool,
|
||||
) -> rusqlite::Result<Self> {
|
||||
let conn = if testing {
|
||||
Connection::open_in_memory()
|
||||
) -> Result<Self> {
|
||||
let connection_manager = if testing {
|
||||
SqliteConnectionManager::memory()
|
||||
} else {
|
||||
Connection::open(DATABASE_FILENAME)
|
||||
}?;
|
||||
SqliteConnectionManager::file(DATABASE_FILENAME)
|
||||
};
|
||||
let connection_pool = Pool::new(connection_manager)?;
|
||||
let conn = connection_pool.get()?;
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS Submission (
|
||||
id INTEGER PRIMARY KEY,
|
||||
|
@ -46,21 +62,26 @@ impl Database {
|
|||
)",
|
||||
params![],
|
||||
)?;
|
||||
Ok(Self { conn })
|
||||
Ok(Self { connection_pool })
|
||||
}
|
||||
|
||||
pub fn has_submitted(&self, user_id: u64) -> rusqlite::Result<bool> {
|
||||
Ok(self.conn
|
||||
fn conn(&self) -> std::result::Result<PooledConnection<SqliteConnectionManager>, r2d2::Error> {
|
||||
self.connection_pool.get()
|
||||
}
|
||||
|
||||
pub fn has_submitted(&self, user_id: u64) -> Result<bool> {
|
||||
Ok(self.conn()?
|
||||
.prepare("SELECT 1 FROM User WHERE id = ?1 LIMIT 1")?
|
||||
.query_row(params![user_id], |_row| Ok(()))
|
||||
.is_ok())
|
||||
}
|
||||
|
||||
pub async fn load_legacy(&self, http: &Http) -> rusqlite::Result<()> {
|
||||
pub async fn load_legacy(&self, http: &Http) -> Result<()> {
|
||||
let latest_challenge = get_challenge_number();
|
||||
// HashMap of archived users that are no longer sharing a server with 字ちゃん
|
||||
// Their historical usernames and discriminators will be used
|
||||
let mut archived_users = HashMap::new();
|
||||
let conn = self.conn()?;
|
||||
for n in 1..=latest_challenge {
|
||||
println!("Loading legacy challenge {n}/{latest_challenge}...");
|
||||
let mut file = File::open(format!("data/challenges/{n}.json")).unwrap();
|
||||
|
@ -72,7 +93,7 @@ impl Database {
|
|||
.map(|legacy| (legacy, legacy.parse().unwrap())) {
|
||||
let mut already_updated = false;
|
||||
for submission in submissions {
|
||||
self.conn.execute(
|
||||
conn.execute(
|
||||
"INSERT INTO Submission(author_id, timestamp, image, challenge) VALUES (?1, ?2, ?3, ?4)",
|
||||
params![
|
||||
&submission.author_id,
|
||||
|
@ -120,7 +141,7 @@ impl Database {
|
|||
} else {
|
||||
match User::fetch(http, submission.author_id).await {
|
||||
Ok(user) => {
|
||||
self.conn.execute(
|
||||
conn.execute(
|
||||
"INSERT INTO User(id, name, discriminator, avatar) VALUES (?1, ?2, ?3, ?4)",
|
||||
params![user.id, user.name, user.discriminator, user.avatar]
|
||||
)?;
|
||||
|
@ -137,6 +158,51 @@ impl Database {
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_challenge_user_data(&self, challenge: u32) -> Result<(Vec<Submission>, HashMap<String, User>)> {
|
||||
let submissions = self.get_submissions(challenge)?;
|
||||
let users = self.get_users({
|
||||
let mut user_ids = HashSet::new();
|
||||
for submission in &submissions {
|
||||
user_ids.insert(submission.author_id);
|
||||
}
|
||||
user_ids
|
||||
})?;
|
||||
Ok((submissions, users))
|
||||
}
|
||||
|
||||
pub fn get_submissions(&self, challenge: u32) -> Result<Vec<Submission>> {
|
||||
Ok(self.conn()?
|
||||
.prepare("SELECT author_id, timestamp, image FROM Submission WHERE challenge = ?1")?
|
||||
.query_map(params![challenge], |row| {
|
||||
Ok(Submission {
|
||||
author_id: row.get(0)?,
|
||||
timestamp: row.get(1)?,
|
||||
image: row.get(2)?,
|
||||
})
|
||||
})?
|
||||
.collect::<std::result::Result<Vec<Submission>, rusqlite::Error>>()?)
|
||||
}
|
||||
|
||||
fn get_users(&self, users: HashSet<u64>) -> Result<HashMap<String, User>> {
|
||||
let conn = self.conn()?;
|
||||
// Not sure why derive_more::From is unable to convert these errors
|
||||
users
|
||||
.iter()
|
||||
// u64 must be converted to String for templates
|
||||
.filter_map(|id| -> Option<Result<(String, User)>> {
|
||||
match conn.prepare("SELECT name, discriminator, avatar FROM User WHERE id = ?1") {
|
||||
Ok(mut statement) => Some(statement.query_row(params![id], |row| Ok((id.to_string(), User {
|
||||
id: *id,
|
||||
name: row.get(0)?,
|
||||
discriminator: row.get(1)?,
|
||||
avatar: row.get(2)?,
|
||||
}))).map_err(|error| DatabaseError::Rusqlite(error))),
|
||||
Err(error) => Some(Err(DatabaseError::Rusqlite(error))),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn refresh_users(&self) -> rusqlite::Result<()> {
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
use chrono::{Utc, DateTime};
|
||||
use serde::Serialize;
|
||||
|
||||
// Challenge submission
|
||||
// In the legacy site version, one submission held 1 or more images.
|
||||
// Now, 1 submission = 1 image, and the leaderboard count will be labeled as "participations"
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct Submission {
|
||||
pub author_id: u64,
|
||||
// Some fields might be empty for legacy submissions
|
||||
|
|
|
@ -18,7 +18,7 @@ pub trait Username {
|
|||
fn username(&self) -> String;
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
#[derive(Default, Deserialize, Debug)]
|
||||
pub struct User {
|
||||
#[serde(deserialize_with = "deserialize_id")]
|
||||
pub id: u64,
|
||||
|
|
|
@ -6,7 +6,7 @@ use rocket_dyn_templates::{context, Template};
|
|||
use crate::{
|
||||
cookies::LANG_COOKIE,
|
||||
i18n::DEFAULT as DEFAULT_LANG,
|
||||
models::{Challenge, Settings, SessionUser},
|
||||
models::{Challenge, Settings, SessionUser, Database},
|
||||
utils::AcceptLanguage,
|
||||
};
|
||||
|
||||
|
@ -15,12 +15,16 @@ pub async fn get_challenge(
|
|||
challenge: u32,
|
||||
cookies: &CookieJar<'_>,
|
||||
settings: &State<Settings>,
|
||||
database: &State<Database>,
|
||||
accept_language: AcceptLanguage,
|
||||
) -> Template {
|
||||
let (submissions, users) = database.get_challenge_user_data(challenge).unwrap();
|
||||
Template::render(
|
||||
"index",
|
||||
context! {
|
||||
challenge,
|
||||
submissions,
|
||||
users,
|
||||
settings: settings.deref(),
|
||||
lang: cookies
|
||||
.get(LANG_COOKIE)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue