Compare commits
No commits in common. "v2" and "main" have entirely different histories.
13 changed files with 388 additions and 2835 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,2 +1,2 @@
|
||||||
/target
|
auth.txt
|
||||||
src/auth.txt
|
out.csv
|
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
|
@ -1,4 +1,3 @@
|
||||||
{
|
{
|
||||||
"rust-analyzer.showUnlinkedFileNotification": false,
|
"deno.enable": true
|
||||||
"rust-analyzer.check.command": "clippy"
|
}
|
||||||
}
|
|
||||||
|
|
2429
Cargo.lock
generated
2429
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
18
Cargo.toml
18
Cargo.toml
|
@ -1,18 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "last_chance_stalker"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
spreadsheet-ods = "0.22.1"
|
|
||||||
reqwest = { version = "0.12.0", features = ["json"] }
|
|
||||||
tokio = { version = "1", features = ["full"] }
|
|
||||||
serde_json = "1.0.114"
|
|
||||||
serde = { version = "1.0.197", features = ["derive"] }
|
|
||||||
scraper = "0.19.0"
|
|
||||||
futures = "0.3.30"
|
|
||||||
lazy_static = "1.4.0"
|
|
||||||
icu_locid = "1.4.0"
|
|
||||||
chrono = "0.4.35"
|
|
253
cli.ts
Normal file
253
cli.ts
Normal file
|
@ -0,0 +1,253 @@
|
||||||
|
import {
|
||||||
|
DOMParser,
|
||||||
|
} from "https://deno.land/x/deno_dom@v0.1.45/deno-dom-wasm.ts";
|
||||||
|
import { writeCSV } from "https://deno.land/x/csv@v0.9.2/mod.ts";
|
||||||
|
|
||||||
|
import {
|
||||||
|
NSDAMemberLookup,
|
||||||
|
NSDAMemberPointEntry,
|
||||||
|
NSDAMemberSearchResult,
|
||||||
|
TabroomEntry,
|
||||||
|
} from "./types.ts";
|
||||||
|
|
||||||
|
import { overrides } from "./constants.ts";
|
||||||
|
|
||||||
|
// Load auth
|
||||||
|
const nsdaAuth = await Deno.readFile("./auth.txt").then((a) =>
|
||||||
|
new TextDecoder().decode(a)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fetch and parse tabroom USX entries
|
||||||
|
const tabroomHtml = await fetch(
|
||||||
|
"https://www.tabroom.com/index/tourn/fields.mhtml?tourn_id=30061&event_id=279207",
|
||||||
|
).then((r) => r.text());
|
||||||
|
|
||||||
|
const dom = new DOMParser().parseFromString(
|
||||||
|
tabroomHtml,
|
||||||
|
"text/html",
|
||||||
|
)!;
|
||||||
|
|
||||||
|
const entries: TabroomEntry[] = [];
|
||||||
|
|
||||||
|
for (const row of dom.querySelectorAll("tr")) {
|
||||||
|
const [institution, location, entry, code] = [...row.childNodes]
|
||||||
|
.filter((n) => n.nodeName === "TD")
|
||||||
|
.map((n) => n.textContent.trim());
|
||||||
|
// Skip invalid entries
|
||||||
|
if (!institution || !location || !entry || !code || entry === "Names TBA") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.push({
|
||||||
|
institution,
|
||||||
|
location,
|
||||||
|
entry,
|
||||||
|
code,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup on NSDA
|
||||||
|
const nsdaSearchResults = await Promise
|
||||||
|
.all(
|
||||||
|
entries.map(async (e) => {
|
||||||
|
const override = overrides[e.entry];
|
||||||
|
|
||||||
|
let nsdaLookup: NSDAMemberSearchResult[] = await fetch(
|
||||||
|
`https://api.speechanddebate.org/v2/search?${
|
||||||
|
new URLSearchParams({
|
||||||
|
q: override?.name ?? e.entry,
|
||||||
|
type: "members",
|
||||||
|
}).toString()
|
||||||
|
}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
authorization: nsdaAuth,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
).then((r) => r.json());
|
||||||
|
|
||||||
|
// Short circuit on overrides
|
||||||
|
if (override !== undefined) {
|
||||||
|
const result = nsdaLookup.find((e) => e.id === override.id);
|
||||||
|
if (result !== undefined) {
|
||||||
|
return { tabroom: e, nsdaSearchResult: result };
|
||||||
|
} else {
|
||||||
|
console.error(`ID override failed for ${e.entry} (${override.id})!`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do our best to filter the results down to one
|
||||||
|
// Start by filtering anything but HS competitors and status === 1
|
||||||
|
// (status 2 appeared on a duplicate, so I assume that marks inactive entries?)
|
||||||
|
nsdaLookup = nsdaLookup.filter((ee) =>
|
||||||
|
ee.realm === "hs" && ee.status === 1
|
||||||
|
);
|
||||||
|
// Then, if that doesn't work, filter by state
|
||||||
|
if (nsdaLookup.length > 1) {
|
||||||
|
nsdaLookup = nsdaLookup.filter((ee) =>
|
||||||
|
ee.school_state?.toLowerCase() ===
|
||||||
|
e.location.replace("/US", "").toLowerCase()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// At this point, collapse equivelent results just in case we *have* found one result
|
||||||
|
if (nsdaLookup.length > 1) {
|
||||||
|
const collapsed: NSDAMemberSearchResult[] = [];
|
||||||
|
for (const entry of nsdaLookup) {
|
||||||
|
if (!collapsed.some((e) => e.id === entry.id)) {
|
||||||
|
collapsed.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nsdaLookup = collapsed;
|
||||||
|
}
|
||||||
|
// Finally, if none of the past filters worked, attempt a school filter
|
||||||
|
let schoolFilterUsed = false;
|
||||||
|
if (nsdaLookup.length > 1) {
|
||||||
|
schoolFilterUsed = true;
|
||||||
|
|
||||||
|
nsdaLookup = nsdaLookup.filter((ee) =>
|
||||||
|
ee.school_name.replace(/[^a-zA-Z0-9 -]/g, "").toLowerCase().includes(
|
||||||
|
e.institution.replace(/[^a-zA-Z0-9 -]/g, "").toLowerCase(),
|
||||||
|
) ||
|
||||||
|
e.institution.replace(/[^a-zA-Z0-9 -]/g, "").toLowerCase().includes(
|
||||||
|
ee.school_name.replace(/[^a-zA-Z0-9 -]/g, "").toLowerCase(),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nsdaLookup.length < 1) {
|
||||||
|
console.error(
|
||||||
|
`No results found for entry "${e.entry}" (with code "${e.code}"), from ${e.institution} in ${
|
||||||
|
e.location.replace("/US", "")
|
||||||
|
}.`,
|
||||||
|
);
|
||||||
|
if (schoolFilterUsed) console.error("SCHOOL-BASED FILTER WAS USED!");
|
||||||
|
return null;
|
||||||
|
} else if (nsdaLookup.length > 1) {
|
||||||
|
console.error(
|
||||||
|
`Multiple entries found for entry "${e.entry}" (with code "${e.code}"), from ${e.institution} in ${
|
||||||
|
e.location.replace("/US", "")
|
||||||
|
}!`,
|
||||||
|
);
|
||||||
|
console.error("---------------------------------------------");
|
||||||
|
console.error(nsdaLookup);
|
||||||
|
console.error("---------------------------------------------");
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return { tabroom: e, nsdaSearchResult: nsdaLookup[0] };
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
).then((p) =>
|
||||||
|
p.filter((e) => e !== null) as {
|
||||||
|
tabroom: TabroomEntry;
|
||||||
|
nsdaSearchResult: NSDAMemberSearchResult;
|
||||||
|
}[]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Convert NSDA searches to true NSDA member objects and point entries to get actual information
|
||||||
|
const nsdaLookups = await Promise.all(
|
||||||
|
nsdaSearchResults.map(async ({ tabroom, nsdaSearchResult }) => ({
|
||||||
|
tabroom,
|
||||||
|
nsdaSearchResult,
|
||||||
|
nsdaMemberLookup: await fetch(
|
||||||
|
"https://api.speechanddebate.org/v2/members/" + nsdaSearchResult.id,
|
||||||
|
{ headers: { authorization: nsdaAuth } },
|
||||||
|
).then((r) => r.json()) as NSDAMemberLookup,
|
||||||
|
nsdaPointsLookup: await fetch(
|
||||||
|
"https://api.speechanddebate.org/v2/members/" + nsdaSearchResult.id +
|
||||||
|
"/points",
|
||||||
|
{ headers: { authorization: nsdaAuth } },
|
||||||
|
).then((r) => r.json()) as NSDAMemberPointEntry[],
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Analyze and display aggregate fetched information
|
||||||
|
console.log(
|
||||||
|
`Represented states:
|
||||||
|
${
|
||||||
|
Object.entries(nsdaLookups.reduce(
|
||||||
|
(acc, l) => ({
|
||||||
|
...acc,
|
||||||
|
[l.tabroom.location.replace("/US", "")]:
|
||||||
|
(acc[l.tabroom.location.replace("/US", "")] ?? 0) + 1,
|
||||||
|
}),
|
||||||
|
{} as Record<string, number>,
|
||||||
|
)).sort((a, b) => b[1] - a[1]).map(([state, n]) => "- " + state + ": " + n)
|
||||||
|
.join("\n")
|
||||||
|
}
|
||||||
|
Total entries: ${nsdaLookups.length}
|
||||||
|
Total schools: ${
|
||||||
|
[...new Set(nsdaLookups.map((l) => l.nsdaSearchResult.school_name))].length
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Collect information into a spreadsheet format
|
||||||
|
const sheet = [
|
||||||
|
[
|
||||||
|
"Last points entry",
|
||||||
|
"First name",
|
||||||
|
"Middle name",
|
||||||
|
"Last name",
|
||||||
|
"State",
|
||||||
|
"School",
|
||||||
|
"Extemp Points",
|
||||||
|
"Year Extemp Points",
|
||||||
|
"Extemp tournaments",
|
||||||
|
"Year Extemp Tournaments",
|
||||||
|
"Average Placing",
|
||||||
|
"Year Average Placing",
|
||||||
|
],
|
||||||
|
...nsdaLookups.map((l) => [
|
||||||
|
l.nsdaMemberLookup.last_points_entry,
|
||||||
|
l.nsdaMemberLookup.first,
|
||||||
|
l.nsdaMemberLookup.middle ?? "",
|
||||||
|
l.nsdaMemberLookup.last,
|
||||||
|
l.nsdaSearchResult.school_state ?? l.tabroom.location.replace("/US", ""),
|
||||||
|
l.nsdaSearchResult.school_name,
|
||||||
|
l.nsdaPointsLookup.filter((e) =>
|
||||||
|
e.category_id === 202 /* Extemporaneous speech event ID */
|
||||||
|
).reduce((acc, e) => acc + e.points, 0).toString(),
|
||||||
|
l.nsdaPointsLookup.filter((e) =>
|
||||||
|
e.category_id === 202 && /* Extemporaneous speech event ID */
|
||||||
|
new Date(e.start).getTime() > new Date(2023, 6, 0).getTime()
|
||||||
|
).reduce((acc, e) => acc + e.points, 0).toString(),
|
||||||
|
l.nsdaPointsLookup.filter((e) =>
|
||||||
|
e.category_id === 202 /* Extemporaneous speech event ID */
|
||||||
|
).reduce(
|
||||||
|
(acc, e) => [...acc, ...(acc.includes(e.tourn_id) ? [] : [e.tourn_id])],
|
||||||
|
[] as number[],
|
||||||
|
).length.toString(),
|
||||||
|
l.nsdaPointsLookup.filter((e) =>
|
||||||
|
e.category_id === 202 && /* Extemporaneous speech event ID */
|
||||||
|
new Date(e.start).getTime() > new Date(2023, 6, 0).getTime()
|
||||||
|
).reduce(
|
||||||
|
(acc, e) => [...acc, ...(acc.includes(e.tourn_id) ? [] : [e.tourn_id])],
|
||||||
|
[] as number[],
|
||||||
|
).length.toString(),
|
||||||
|
(Math.round(
|
||||||
|
l.nsdaPointsLookup.filter((e) =>
|
||||||
|
e.category_id === 202 /* Extemporaneous speech event ID */
|
||||||
|
).flatMap((e) => (JSON.parse(e.result) as { ranks: number[] }).ranks)
|
||||||
|
.reduce((acc, v, _, a) => (acc + v / a.length), 0) * 1000,
|
||||||
|
) / 1000).toString(),
|
||||||
|
(Math.round(
|
||||||
|
l.nsdaPointsLookup.filter((e) =>
|
||||||
|
e.category_id === 202 && /* Extemporaneous speech event ID */
|
||||||
|
new Date(e.start).getTime() > new Date(2023, 6, 0).getTime()
|
||||||
|
).flatMap((e) => (JSON.parse(e.result) as { ranks: number[] }).ranks)
|
||||||
|
.reduce((acc, v, _, a) => (acc + v / a.length), 0) * 1000,
|
||||||
|
) / 1000).toString(),
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
|
||||||
|
await writeCSV(
|
||||||
|
await Deno.open("./out.csv", {
|
||||||
|
write: true,
|
||||||
|
create: true,
|
||||||
|
truncate: true,
|
||||||
|
}),
|
||||||
|
sheet,
|
||||||
|
{
|
||||||
|
columnSeparator: "\t",
|
||||||
|
},
|
||||||
|
);
|
9
constants.ts
Normal file
9
constants.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
export const overrides: Record<
|
||||||
|
string,
|
||||||
|
{ name: string; id: number } | undefined
|
||||||
|
> = {
|
||||||
|
"Christopher Ramirez-Chavez": {
|
||||||
|
name: "Christopher Chavez",
|
||||||
|
id: 10847338,
|
||||||
|
},
|
||||||
|
};
|
BIN
out.ods
BIN
out.ods
Binary file not shown.
71
src/main.rs
71
src/main.rs
|
@ -1,71 +0,0 @@
|
||||||
mod nsda;
|
|
||||||
mod tabroom;
|
|
||||||
mod types;
|
|
||||||
mod spreadsheet;
|
|
||||||
|
|
||||||
use futures::future;
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
|
|
||||||
use crate::{nsda::NSDASearchResponse, types::EntryData};
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
pub static ref CLIENT: reqwest::Client = reqwest::Client::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
// println!("Fetching entries...");
|
|
||||||
|
|
||||||
// let entries = tabroom::fetch_entries(30061, 279207).await;
|
|
||||||
// println!("Fetched entries, searching with NSDA...");
|
|
||||||
// let searches = future::join_all(
|
|
||||||
// entries
|
|
||||||
// .iter()
|
|
||||||
// .map(|entry| Box::pin(nsda::search_member(&entry.entry))),
|
|
||||||
// )
|
|
||||||
// .await;
|
|
||||||
// // If many were found, filter, if one was found, use it, if none were found, quietly error
|
|
||||||
// let filtered = searches.into_iter().zip(entries.into_iter()).filter_map(|(search_results, entry)| {
|
|
||||||
// match search_results {
|
|
||||||
// NSDASearchResponse::Many(results) => {
|
|
||||||
// let filtered = nsda::filter_search_results(
|
|
||||||
// results,
|
|
||||||
// &entry.location.replace("/US", ""),
|
|
||||||
// &entry.institution,
|
|
||||||
// );
|
|
||||||
// match filtered {
|
|
||||||
// NSDASearchResponse::None => {
|
|
||||||
// eprintln!("Unable to narrow down search results for entry {}, none remained after filtering", entry.entry);
|
|
||||||
// None
|
|
||||||
// }
|
|
||||||
// NSDASearchResponse::Many(filtered) => {
|
|
||||||
// eprintln!("Unable to narrow down search results for entry {}, multiple were left after fitlering: {}", entry.entry, filtered.into_iter().map(|r| r.id.to_string()).collect::<Vec<_>>().join(", "));
|
|
||||||
// None
|
|
||||||
// },
|
|
||||||
// NSDASearchResponse::One(result) => {
|
|
||||||
// Some((entry, *result))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// NSDASearchResponse::None => {
|
|
||||||
// eprintln!("Unable to find any search results for entry {} without filtering", entry.entry);
|
|
||||||
// None
|
|
||||||
// },
|
|
||||||
// NSDASearchResponse::One(result) => Some((entry, *result))
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// println!("Searching done, looking up member information and point records...");
|
|
||||||
// let queried = future::join_all(filtered.map(|(entry, search_result)| async {
|
|
||||||
// EntryData {
|
|
||||||
// member: nsda::fetch_member(search_result.id).await,
|
|
||||||
// points: nsda::fetch_member_points(search_result.id).await,
|
|
||||||
// entry,
|
|
||||||
// search_result,
|
|
||||||
// }
|
|
||||||
// }))
|
|
||||||
// .await;
|
|
||||||
|
|
||||||
// Format to spreadsheet or shit
|
|
||||||
let mut workbook = spreadsheet::generate_spreadsheet(/*queried*/);
|
|
||||||
spreadsheet_ods::write_ods(&mut workbook, "./out.ods").unwrap();
|
|
||||||
}
|
|
89
src/nsda.rs
89
src/nsda.rs
|
@ -1,89 +0,0 @@
|
||||||
use crate::{
|
|
||||||
types::{NSDAMember, NSDAMemberPointEntry, NSDAMemberSearchResult},
|
|
||||||
CLIENT,
|
|
||||||
};
|
|
||||||
|
|
||||||
const NSDA_AUTH: &str = include_str!("auth.txt");
|
|
||||||
|
|
||||||
pub enum NSDASearchResponse {
|
|
||||||
One(Box<NSDAMemberSearchResult>),
|
|
||||||
Many(Vec<NSDAMemberSearchResult>),
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn search_member<S: AsRef<str>>(query: S) -> NSDASearchResponse {
|
|
||||||
let mut results = CLIENT
|
|
||||||
.get("https://api.speechanddebate.org/v2/search")
|
|
||||||
.query(&[("q", query.as_ref()), ("type", "members")])
|
|
||||||
.header("Authorization", NSDA_AUTH)
|
|
||||||
.header("Accept", "application/json")
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.json::<Vec<NSDAMemberSearchResult>>()
|
|
||||||
.await
|
|
||||||
.unwrap_or_else(|_| { panic!("{}", query.as_ref().to_string()) });
|
|
||||||
|
|
||||||
match results.len() {
|
|
||||||
0 => NSDASearchResponse::None,
|
|
||||||
1 => NSDASearchResponse::One(Box::new(results.remove(0))),
|
|
||||||
_ => NSDASearchResponse::Many(results),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn filter_search_results<S: AsRef<str>>(
|
|
||||||
mut results: Vec<NSDAMemberSearchResult>,
|
|
||||||
state: S,
|
|
||||||
school: S,
|
|
||||||
) -> NSDASearchResponse {
|
|
||||||
// First, filter by HS vs. MS results and stats==1 (which appears to be true for non-duplicates)
|
|
||||||
results.retain(|result| result.realm == Some("hs".to_string()) && result.status == Some(1));
|
|
||||||
if results.len() == 1 {
|
|
||||||
return NSDASearchResponse::One(Box::new(results.remove(0)));
|
|
||||||
}
|
|
||||||
// If that doesn't work, filter by state
|
|
||||||
results.retain(|result| result.school_state.as_ref().is_some_and(|s| s == state.as_ref()));
|
|
||||||
if results.len() == 1 {
|
|
||||||
return NSDASearchResponse::One(Box::new(results.remove(0)));
|
|
||||||
}
|
|
||||||
// And finally, as a last case scenario
|
|
||||||
results.retain(|result| {
|
|
||||||
result
|
|
||||||
.school_name
|
|
||||||
.to_lowercase()
|
|
||||||
.contains(&school.as_ref().to_lowercase())
|
|
||||||
});
|
|
||||||
match results.len() {
|
|
||||||
0 => NSDASearchResponse::None,
|
|
||||||
1 => NSDASearchResponse::One(Box::new(results.remove(0))),
|
|
||||||
_ => NSDASearchResponse::Many(results),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn fetch_member(id: usize) -> NSDAMember {
|
|
||||||
CLIENT
|
|
||||||
.get(format!("https://api.speechanddebate.org/v2/members/{id}"))
|
|
||||||
.header("Authorization", NSDA_AUTH)
|
|
||||||
.header("Accept", "application/json")
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.json::<NSDAMember>()
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn fetch_member_points(id: usize) -> Vec<NSDAMemberPointEntry> {
|
|
||||||
CLIENT
|
|
||||||
.get(format!(
|
|
||||||
"https://api.speechanddebate.org/v2/members/{id}/points"
|
|
||||||
))
|
|
||||||
.header("Authorization", NSDA_AUTH)
|
|
||||||
.header("Accept", "application/json")
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.json::<Vec<NSDAMemberPointEntry>>()
|
|
||||||
.await
|
|
||||||
.unwrap_or_else(|_| { panic!("{}", id.to_string()) })
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
use crate::types::EntryData;
|
|
||||||
use chrono::Utc;
|
|
||||||
use icu_locid::locale;
|
|
||||||
use spreadsheet_ods::{defaultstyles::DefaultFormat, format::ValueFormatTrait, style::units::{FontSize, WrapOption}, CellStyle, Length, Sheet, Value, ValueFormatDateTime, WorkBook};
|
|
||||||
|
|
||||||
pub fn generate_spreadsheet(/*data: Vec<EntryData>*/) -> WorkBook {
|
|
||||||
let mut workbook = WorkBook::new(locale!("en_US"));
|
|
||||||
|
|
||||||
// Setup styles
|
|
||||||
let mut large_style = CellStyle::new("largestyle", &DefaultFormat::default());
|
|
||||||
large_style.set_font_bold();
|
|
||||||
large_style.set_font_size(FontSize::Length(spreadsheet_ods::Length::Pt(16f64)));
|
|
||||||
large_style.set_wrap_option(WrapOption::Wrap);
|
|
||||||
let large_style_ref = large_style.style_ref();
|
|
||||||
workbook.add_cellstyle(large_style);
|
|
||||||
|
|
||||||
let mut bold_style = CellStyle::new("boldstyle", &DefaultFormat::default());
|
|
||||||
bold_style.set_font_bold();
|
|
||||||
bold_style.set_wrap_option(WrapOption::Wrap);
|
|
||||||
let bold_style_ref = bold_style.style_ref();
|
|
||||||
workbook.add_cellstyle(bold_style);
|
|
||||||
|
|
||||||
// let mut datetime_value_format = ValueFormatDateTime::new_named("datetimevalueformat");
|
|
||||||
// datetime_value_format.part_day_of_week().build();
|
|
||||||
// datetime_value_format.part_text(" ").build();
|
|
||||||
// datetime_value_format.part_month().build();
|
|
||||||
// datetime_value_format.part_text("/").build();
|
|
||||||
// datetime_value_format.part_day().build();
|
|
||||||
// let datetime_value_format_ref = datetime_value_format.format_ref();
|
|
||||||
// workbook.add_datetime_format(datetime_value_format);
|
|
||||||
// let datetime_format = CellStyle::new("datetimeformat", &datetime_value_format_ref);
|
|
||||||
// let datetime_format_ref = datetime_format.style_ref();
|
|
||||||
let mut format = workbook.datetime_format("datetime1").unwrap().clone();
|
|
||||||
format.part_text("hi").build();
|
|
||||||
workbook.remove_datetime_format("datetime1");
|
|
||||||
workbook.add_datetime_format(format);
|
|
||||||
|
|
||||||
|
|
||||||
// Main info spreadsheet
|
|
||||||
let mut info = Sheet::new("Information");
|
|
||||||
info.set_col_width(0, Length::In(4f64));
|
|
||||||
info.set_styled_value(0, 0, "This data is fetched and calculated automatically, though updates must be entered into google sheets manually (for now? who knows)", &large_style_ref);
|
|
||||||
info.set_value(1, 0, "If its outdated yell at Tyler or smth");
|
|
||||||
info.set_value(2, 0, "You can see the data by switching sheets below.");
|
|
||||||
info.set_styled_value(0, 1, "Last update:", &bold_style_ref);
|
|
||||||
info.set_value(1, 1, Utc::now().naive_utc());
|
|
||||||
|
|
||||||
// Add all sheets together
|
|
||||||
workbook.push_sheet(info);
|
|
||||||
|
|
||||||
workbook
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
use crate::{types::TabroomEntry, CLIENT};
|
|
||||||
use scraper::{Html, Selector};
|
|
||||||
|
|
||||||
pub async fn fetch_entries(tournament_id: usize, event_id: usize) -> Vec<TabroomEntry> {
|
|
||||||
let html = CLIENT
|
|
||||||
.get("https://www.tabroom.com/index/tourn/fields.mhtml")
|
|
||||||
.query(&[("tourn_id", tournament_id), ("event_id", event_id)])
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.text()
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let parsed = Html::parse_document(&html);
|
|
||||||
let selector = Selector::parse("tr").unwrap();
|
|
||||||
|
|
||||||
Vec::from_iter(parsed.select(&selector).map(|row| {
|
|
||||||
let mut children = row.child_elements();
|
|
||||||
|
|
||||||
TabroomEntry {
|
|
||||||
institution: children.next().unwrap().inner_html().trim().to_string(),
|
|
||||||
location: children.next().unwrap().inner_html().trim().to_string(),
|
|
||||||
entry: children.next().unwrap().inner_html().trim().to_string(),
|
|
||||||
code: children.next().unwrap().inner_html().trim().to_string(),
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
143
src/types.rs
143
src/types.rs
|
@ -1,143 +0,0 @@
|
||||||
use serde::Deserialize;
|
|
||||||
use serde_json::Value;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct EntryData {
|
|
||||||
pub entry: TabroomEntry,
|
|
||||||
pub search_result: NSDAMemberSearchResult,
|
|
||||||
pub member: NSDAMember,
|
|
||||||
pub points: Vec<NSDAMemberPointEntry>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A USX entry on tabroom
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct TabroomEntry {
|
|
||||||
pub institution: String,
|
|
||||||
pub location: String,
|
|
||||||
pub entry: String,
|
|
||||||
pub code: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct NSDAMemberSearchResult {
|
|
||||||
pub id: usize,
|
|
||||||
pub first: String,
|
|
||||||
pub last: String,
|
|
||||||
pub role: Option<String>,
|
|
||||||
pub school_id: Option<usize>,
|
|
||||||
pub status: Option<usize>,
|
|
||||||
pub realm: Option<String>,
|
|
||||||
pub school_name: String,
|
|
||||||
pub school_state: Option<String>,
|
|
||||||
pub r#type: String,
|
|
||||||
pub name: String,
|
|
||||||
pub description: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct NSDAMember {
|
|
||||||
pub person_id: usize,
|
|
||||||
pub first: String,
|
|
||||||
pub middle: Option<String>,
|
|
||||||
pub last: String,
|
|
||||||
pub disabled: usize,
|
|
||||||
pub created_at: String,
|
|
||||||
pub points: usize,
|
|
||||||
pub points_service: usize,
|
|
||||||
pub points_service_this_year: usize,
|
|
||||||
pub points_this_year: usize,
|
|
||||||
pub points_last_year: usize,
|
|
||||||
pub last_points_entry: String,
|
|
||||||
pub citation_points: usize,
|
|
||||||
pub degree_id: usize,
|
|
||||||
pub degree_name: String,
|
|
||||||
pub to_next_degree: usize,
|
|
||||||
pub diamonds: Option<usize>,
|
|
||||||
#[serde(rename = "3_diamond")]
|
|
||||||
pub n3_diamond: bool,
|
|
||||||
pub hof: bool,
|
|
||||||
pub aaa: bool,
|
|
||||||
pub active: bool,
|
|
||||||
pub paid: bool,
|
|
||||||
pub paid_latest: String,
|
|
||||||
pub school_paid: bool,
|
|
||||||
pub active_student: bool,
|
|
||||||
pub degrees: Vec<Degree>,
|
|
||||||
pub honors: Vec<Honor>,
|
|
||||||
pub citations: Vec<Value>,
|
|
||||||
pub districts_eligible: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct Degree {
|
|
||||||
pub member_honor_id: usize,
|
|
||||||
pub person_id: usize,
|
|
||||||
pub honor_id: usize,
|
|
||||||
pub description: String,
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
pub type_field: String,
|
|
||||||
pub school_id: usize,
|
|
||||||
pub school_name: String,
|
|
||||||
pub district_id: Option<String>,
|
|
||||||
pub district_name: Option<String>,
|
|
||||||
pub state: Option<String>,
|
|
||||||
pub points: usize,
|
|
||||||
pub start: String,
|
|
||||||
pub end: Option<String>,
|
|
||||||
pub status: String,
|
|
||||||
pub recognized: Value,
|
|
||||||
pub note: Option<String>,
|
|
||||||
pub created_at: String,
|
|
||||||
pub created_by_id: Option<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct Honor {
|
|
||||||
pub member_honor_id: usize,
|
|
||||||
pub person_id: usize,
|
|
||||||
pub honor_id: usize,
|
|
||||||
pub description: String,
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
pub type_field: String,
|
|
||||||
pub school_id: usize,
|
|
||||||
pub school_name: String,
|
|
||||||
pub district_id: Option<usize>,
|
|
||||||
pub district_name: Option<String>,
|
|
||||||
pub state: Option<String>,
|
|
||||||
pub points: Option<usize>,
|
|
||||||
pub start: String,
|
|
||||||
pub end: Option<String>,
|
|
||||||
pub status: String,
|
|
||||||
pub recognized: Value,
|
|
||||||
pub note: Option<String>,
|
|
||||||
pub created_at: String,
|
|
||||||
pub created_by_id: Option<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct NSDAMemberPointEntry {
|
|
||||||
pub id: usize,
|
|
||||||
pub student_id: usize,
|
|
||||||
pub student_name: String,
|
|
||||||
pub coach_id: Option<usize>,
|
|
||||||
pub coach_name: Option<String>,
|
|
||||||
pub school_id: usize,
|
|
||||||
pub category_id: usize,
|
|
||||||
pub points: usize,
|
|
||||||
pub result: Option<String>,
|
|
||||||
pub description: Option<String>,
|
|
||||||
pub autopost: bool,
|
|
||||||
pub districts: bool,
|
|
||||||
pub nationals: bool,
|
|
||||||
pub tourn_id: usize,
|
|
||||||
pub tourn_name: String,
|
|
||||||
pub location: String,
|
|
||||||
pub state: String,
|
|
||||||
pub start: String,
|
|
||||||
pub end: String,
|
|
||||||
pub source: String,
|
|
||||||
pub status: usize,
|
|
||||||
pub created_at: String,
|
|
||||||
pub created_by_id: Option<usize>,
|
|
||||||
pub created_by: String,
|
|
||||||
}
|
|
122
types.ts
Normal file
122
types.ts
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
export interface NSDAMemberSearchResult {
|
||||||
|
id: number;
|
||||||
|
first: string;
|
||||||
|
last: string;
|
||||||
|
role: string;
|
||||||
|
school_id: number;
|
||||||
|
status: number;
|
||||||
|
realm: string;
|
||||||
|
school_name: string;
|
||||||
|
school_state: string | null;
|
||||||
|
type: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TabroomEntry {
|
||||||
|
institution: string;
|
||||||
|
location: string;
|
||||||
|
entry: string;
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NSDAMemberLookup {
|
||||||
|
person_id: number;
|
||||||
|
first: string;
|
||||||
|
middle: string | null;
|
||||||
|
last: string;
|
||||||
|
disabled: number;
|
||||||
|
created_at: string;
|
||||||
|
points: number;
|
||||||
|
points_service: number;
|
||||||
|
points_service_this_year: number;
|
||||||
|
points_this_year: number;
|
||||||
|
points_last_year: number;
|
||||||
|
last_points_entry: string;
|
||||||
|
citation_points: number;
|
||||||
|
degree_id: number;
|
||||||
|
degree_name: string;
|
||||||
|
to_next_degree: number;
|
||||||
|
diamonds: number | null;
|
||||||
|
"3_diamond": boolean;
|
||||||
|
hof: boolean;
|
||||||
|
aaa: boolean;
|
||||||
|
active: boolean;
|
||||||
|
paid: boolean;
|
||||||
|
paid_latest: string;
|
||||||
|
school_paid: boolean;
|
||||||
|
active_student: boolean;
|
||||||
|
degrees: NSDAMemberDegree[];
|
||||||
|
honors: NSDAMemberHonor[];
|
||||||
|
citations: unknown[];
|
||||||
|
districts_eligible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NSDAMemberDegree {
|
||||||
|
member_honor_id: number;
|
||||||
|
person_id: number;
|
||||||
|
honor_id: number;
|
||||||
|
description: string;
|
||||||
|
type: string;
|
||||||
|
school_id: number;
|
||||||
|
school_name: string;
|
||||||
|
district_id: number | null;
|
||||||
|
district_name: string | null;
|
||||||
|
state: string | null;
|
||||||
|
points: number;
|
||||||
|
start: string;
|
||||||
|
end: unknown;
|
||||||
|
status: string;
|
||||||
|
recognized: unknown;
|
||||||
|
note: string | null;
|
||||||
|
created_at: string;
|
||||||
|
created_by_id: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NSDAMemberHonor {
|
||||||
|
member_honor_id: number;
|
||||||
|
person_id: number;
|
||||||
|
honor_id: number;
|
||||||
|
description: string;
|
||||||
|
type: string;
|
||||||
|
school_id: number;
|
||||||
|
school_name: string;
|
||||||
|
district_id: number | null;
|
||||||
|
district_name: string | null;
|
||||||
|
state: string | null;
|
||||||
|
points: number | null;
|
||||||
|
start: string;
|
||||||
|
end: unknown;
|
||||||
|
status: string;
|
||||||
|
recognized: unknown;
|
||||||
|
note: string | null;
|
||||||
|
created_at: string;
|
||||||
|
created_by_id: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NSDAMemberPointEntry {
|
||||||
|
id: number;
|
||||||
|
student_id: number;
|
||||||
|
student_name: string;
|
||||||
|
coach_id: number;
|
||||||
|
coach_name: string;
|
||||||
|
school_id: number;
|
||||||
|
category_id: number;
|
||||||
|
points: number;
|
||||||
|
result: string;
|
||||||
|
description: string | null;
|
||||||
|
autopost: boolean;
|
||||||
|
districts: boolean;
|
||||||
|
nationals: boolean;
|
||||||
|
tourn_id: number;
|
||||||
|
tourn_name: string;
|
||||||
|
location: string;
|
||||||
|
state: string;
|
||||||
|
start: string;
|
||||||
|
end: string;
|
||||||
|
source: string;
|
||||||
|
status: number;
|
||||||
|
created_at: string;
|
||||||
|
created_by_id: number | null;
|
||||||
|
created_by: string;
|
||||||
|
}
|
Loading…
Reference in a new issue