Skip to content
This repository has been archived by the owner on Nov 28, 2023. It is now read-only.

Commit

Permalink
Merge pull request #3 from AOx0/askama
Browse files Browse the repository at this point in the history
Askama
  • Loading branch information
AOx0 authored Sep 12, 2023
2 parents a1c5cf0 + 0832497 commit 2bb94a8
Show file tree
Hide file tree
Showing 16 changed files with 18,121 additions and 28 deletions.
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
tailwind.config.js linguist-vendored
style.css linguist-vendored
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ dhat-*
*.csv
*.lock
*.json
results/*
results/*
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ edition = "2021"

[dependencies]
anyhow = "1.0.72"
askama = { version = "0.12.0", features = ["with-axum"] }
askama_axum = "0.3.0"
axum = { version = "0.6.20", features = ["macros"] }
axum-extra = { version = "0.7.7", features = ["protobuf"] }
chrono = "0.4.26"
dhat = { version = "0.3.2", optional = true }
dotenv = "0.15.0"
prost = "0.12.0"
Expand Down
17 changes: 17 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use std::process::Command;

macro_rules! p {
($($tokens: tt)*) => {
println!("cargo:warning={}", format!($($tokens)*))
}
}

fn main() {
let res = Command::new("tailwindcss")
.args(["-i", "./templates/input.css", "-o", "./style.css"])
.output();

if let Err(err) = res {
p!("Error executing `tailwindcss` {}. Skipping.", err);
}
}
5 changes: 5 additions & 0 deletions cdn.min.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions htmx.min.js

Large diffs are not rendered by default.

253 changes: 226 additions & 27 deletions src/bin/api.rs
Original file line number Diff line number Diff line change
@@ -1,47 +1,238 @@
use askama::Template;
use axum::extract::{Path, State};
use axum::routing::get;
use axum::{Router, Server};
use axum_extra::protobuf::Protobuf;
use axum::http::{header, HeaderName};
use axum::response::{AppendHeaders, IntoResponse};
use axum::routing::{get, post};
use axum::{Json, Router, Server};
use chrono::prelude::*;
use dotenv::dotenv;
use serde::Serialize;
use serde::{Deserialize, Serialize};
use sqlx::mysql::MySqlPoolOptions;
use sqlx::{MySql, Pool};
use std::borrow::Cow;
use std::sync::Arc;

#[derive(prost::Message)]
const JS_HEADER: AppendHeaders<[(HeaderName, &str); 1]> =
AppendHeaders([(header::CONTENT_TYPE, "text/javascript")]);

const CSS_HEADER: AppendHeaders<[(HeaderName, &str); 1]> =
AppendHeaders([(header::CONTENT_TYPE, "text/css")]);

const SVG_HEADER: AppendHeaders<[(HeaderName, &str); 1]> =
AppendHeaders([(header::CONTENT_TYPE, "image/svg+xml")]);
#[derive(Serialize)]
struct Test {
#[prost(string, tag = "1")]
nombre: String,
#[prost(string, tag = "2")]
resultado: String,
}

async fn date(
struct Content {
name: &'static str,
content: &'static str,
desc: &'static str,
method: &'static str,
}

struct Section {
name: &'static str,
href: &'static str,
}

#[derive(Template)]
#[template(path = "hello.html")]
struct Hello<'a> {
posts: &'a [Content],
sects: &'a [Section],
}

async fn root() -> Hello<'static> {
Hello {
posts: &[
Content {
name: "Muertos",
content: " ",
desc: "+0.12 de la semana pasada",
method: "/date/2023-02-23",
},
Content {
name: "Robos",
content: " ",
desc: "Robos armados",
method: "/date/2023-02-24",
},
Content {
name: "Homicidios",
content: " ",
desc: "En esta semana",
method: "/date/2023-02-25",
},
Content {
name: "Carpetas",
content: " ",
desc: "En esta año",
method: "/date/upnow",
},
],
sects: &[Section {
name: "Zonas calientes",
href: "#",
}],
}
}

async fn htmx() -> impl IntoResponse {
(
JS_HEADER,
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/htmx.min.js")),
)
}

async fn alpine() -> impl IntoResponse {
(
JS_HEADER,
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/cdn.min.js")),
)
}

async fn mapa(Path(n): Path<usize>) -> impl IntoResponse {
(
SVG_HEADER,
format!(
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/MXfmt.svg")),
n
),
)
}

async fn scripts() -> impl IntoResponse {
(
JS_HEADER,
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/script.js")),
)
}

async fn tailwind() -> impl IntoResponse {
(
CSS_HEADER,
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/style.css")),
)
}

async fn date(State(state): State<Shared>, Path(date): Path<String>) -> String {
let invalid = date.chars().any(|a| !(a.is_ascii_digit() || a == '-'));

if invalid {
return format!("Invalid date '{date}'");
}

let row: Result<(String,), _> =
sqlx::query_as("SELECT FORMAT((SELECT COUNT(1) FROM delitos WHERE fecha_hecho = ?), 0)")
.bind(&date)
.fetch_one(&state.db)
.await;

match row {
Ok((row,)) => format!("+{}", row),
Err(_) => "INTERR".to_string(),
}
}

#[derive(Serialize, Debug, Default)]
struct MapaPorcetajes {
total: u64,
valores: Vec<u64>,
}

#[derive(Debug, Deserialize)]
struct SolicitudMapaPorcentajes {
#[serde(default = "min_year")]
annio_inicio: u16,
#[serde(default = "max_year")]
annio_final: u16,
#[serde(default)]
categorias: Vec<u16>,
}

fn min_year() -> u16 {
2016
}
fn max_year() -> u16 {
2023
}

/// TODO: This seems expensive, benchmark and optimize
///
/// # Panics
///
/// Panics if .
async fn mapa_porcentajes(
State(state): State<Shared>,
Path(date): Path<String>,
) -> Result<Protobuf<Test>, Protobuf<Test>> {
let row: Result<(i64,String), _> = sqlx::query_as(
"SELECT COUNT(1), DATE_FORMAT(?, '%Y-%m-%d') FROM delitos WHERE fecha_hecho = ? GROUP BY fecha_hecho",
Json(sol): Json<SolicitudMapaPorcentajes>,
) -> Json<MapaPorcetajes> {
let SolicitudMapaPorcentajes {
annio_inicio,
annio_final,
categorias,
} = sol;

let (total,): (i64,) = if categorias.is_empty() {
sqlx::query_as(&format!("SELECT COUNT(1) FROM delitos WHERE fecha_hecho BETWEEN '{annio_inicio}-01-01' AND '{annio_final}-12-31';"))
.fetch_one(&state.db)
.await
.unwrap()
} else {
sqlx::query_as(&format!("SELECT COUNT(1) FROM delitos WHERE delitos.id_categoria IN ({0}) AND fecha_hecho BETWEEN '{annio_inicio}-01-01' AND '{annio_final}-12-31';",
categorias
.iter()
.map(|id| format!("{id}"))
.collect::<Vec<_>>()
.join(",")
))
.fetch_one(&state.db)
.await
.unwrap()
};

let resultados: Vec<(i64,)> = if categorias.is_empty() {
sqlx::query_as(&format!("SELECT COUNT(1) FROM delitos WHERE fecha_hecho BETWEEN '{annio_inicio}-01-01' AND '{annio_final}-12-31' AND delitos.id_alcaldia_hecho IS NOT NULL GROUP BY delitos.id_alcaldia_hecho ORDER BY delitos.id_alcaldia_hecho;"))
.fetch_all(&state.db)
.await
.unwrap()
} else {
sqlx::query_as(&format!("SELECT COUNT(1) FROM delitos WHERE delitos.id_categoria IN ({0}) AND fecha_hecho BETWEEN '{annio_inicio}-01-01' AND '{annio_final}-12-31' AND delitos.id_alcaldia_hecho IS NOT NULL GROUP BY delitos.id_alcaldia_hecho ORDER BY delitos.id_alcaldia_hecho;",
categorias
.iter()
.map(|id| format!("{id}"))
.collect::<Vec<_>>()
.join(",")
))
.fetch_all(&state.db)
.await
.unwrap()
};

MapaPorcetajes {
total: u64::try_from(total).unwrap(),
valores: resultados.into_iter().map(|(n,)| n as u64).collect(),
}
.into()
}

async fn untilnow(State(state): State<Shared>) -> String {
let utc: DateTime<Utc> = Utc::now();
let year = utc.format("%Y").to_string();

let row: Result<(String,), _> = sqlx::query_as(
"SELECT FORMAT((SELECT COUNT(1) FROM delitos WHERE YEAR(fecha_hecho) = ?), 0)",
)
.bind(&date)
.bind(&date)
.bind(&year)
.fetch_one(&state.db)
.await;

match row {
Ok((row, date)) => Ok(Test {
nombre: date.into(),
resultado: format!("{}", row),
}
.into()),
Ok((row,)) => format!("+{}", row),
Err(err) => {
eprintln!("Error with query 'date' (date: {date}): {err}");
Err(Test {
nombre: "Error".into(),
resultado: "Internal error".to_string(),
}
.into())
println!("Error: {err}");
"INTERR".to_string()
}
}
}
Expand Down Expand Up @@ -77,8 +268,16 @@ async fn main() -> anyhow::Result<()> {
}));

let router = Router::new()
.route("/date/:date", get(date))
.route("/", get(root))
.route("/alpine.js", get(alpine))
.route("/tailwind.css", get(tailwind))
.route("/htmx.js", get(htmx))
.route("/script.js", get(scripts))
.route("/mapa/:n", get(mapa))
.route("/health", get(|| async { "alive" }))
.route("/date/:date", get(date))
.route("/map_percent", post(mapa_porcentajes))
.route("/date/upnow", get(untilnow))
.with_state(state);

Server::bind(&address.parse().unwrap())
Expand Down
Loading

0 comments on commit 2bb94a8

Please sign in to comment.