This commit is contained in:
Dominic Grimm 2023-02-12 10:57:02 +01:00
parent 964534d0d9
commit e262fc8ab2
No known key found for this signature in database
GPG key ID: 6F294212DEAAC530
6 changed files with 213 additions and 139 deletions

View file

@ -9,16 +9,6 @@ body {
font-family: "JetBrains Mono", monospace; font-family: "JetBrains Mono", monospace;
} }
// * {
// font-family: "JetBrains Mono", monospace;
// }
h1,
h2,
h3 {
text-align: center;
}
h2 { h2 {
text-decoration: underline; text-decoration: underline;
} }

View file

@ -34,6 +34,8 @@ pub mod keys {
format!("{}{}", POST, id) format!("{}{}", POST, id)
} }
pub const TAGS: &str = "tags";
pub const TAG_POSTS: &str = "tag_posts:"; pub const TAG_POSTS: &str = "tag_posts:";
pub fn tag_posts(id: i32) -> String { pub fn tag_posts(id: i32) -> String {
@ -115,7 +117,7 @@ pub fn cache_posts(
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct Post { pub struct PostWithoutDescription {
pub name: String, pub name: String,
pub slug: String, pub slug: String,
pub published_at: NaiveDate, pub published_at: NaiveDate,
@ -124,11 +126,21 @@ pub struct Post {
pub content: String, pub content: String,
} }
#[derive(Serialize, Deserialize, Debug)]
pub struct PostWithoutContent {
pub name: String,
pub slug: String,
pub description: String,
pub published_at: NaiveDate,
pub edited_at: Option<NaiveDate>,
pub tags: Vec<String>,
}
pub fn cache_post( pub fn cache_post(
id: i32, id: i32,
db_conn: &mut db::Connection, db_conn: &mut db::Connection,
redis_conn: &mut Connection, redis_conn: &mut Connection,
) -> Result<Post> { ) -> Result<PostWithoutDescription> {
let key = keys::post(id); let key = keys::post(id);
if let Some(s) = redis::cmd("GET") if let Some(s) = redis::cmd("GET")
@ -141,7 +153,7 @@ pub fn cache_post(
.filter(db::schema::posts::id.eq(id)) .filter(db::schema::posts::id.eq(id))
.first::<db::models::Post>(db_conn)?; .first::<db::models::Post>(db_conn)?;
let post_cache = Post { let post_cache = PostWithoutDescription {
name: post.name, name: post.name,
slug: post.slug, slug: post.slug,
published_at: post.published_at, published_at: post.published_at,
@ -163,6 +175,98 @@ pub fn cache_post(
} }
} }
#[derive(Serialize, Deserialize, Debug)]
pub struct PostsByTag {
pub name: String,
pub posts: Vec<PostWithoutContent>,
}
pub fn cache_tags(
db_conn: &mut db::Connection,
redis_conn: &mut Connection,
) -> Result<Vec<PostsByTag>> {
if let Some(s) = redis::cmd("GET")
.arg(keys::TAGS)
.query::<Option<String>>(redis_conn)?
{
Ok(serde_json::from_str(&s)?)
} else {
let tags = db::schema::tags::table
.select((db::schema::tags::id, db::schema::tags::name))
.order(db::schema::tags::name)
.load::<(i32, String)>(db_conn)?;
let posts_by_tag = tags
.into_iter()
.map(|(id, name)| -> Result<_> {
let post_tag_post_ids = db::schema::post_tags::table
.select(db::schema::post_tags::post_id)
.filter(db::schema::post_tags::tag_id.eq(id))
.load::<i32>(db_conn)?;
let posts = db::schema::posts::table
.select((
db::schema::posts::id,
db::schema::posts::name,
db::schema::posts::slug,
db::schema::posts::description,
db::schema::posts::published_at,
db::schema::posts::edited_at,
))
.filter(db::schema::posts::active)
.filter(db::schema::posts::id.eq_any(post_tag_post_ids))
.order(db::schema::posts::published_at.desc())
.load::<(i32, String, String, String, NaiveDate, Option<NaiveDate>)>(db_conn)?;
Ok(
PostsByTag {
name,
posts:
posts
.into_iter()
.map(
|p: (
i32,
String,
String,
String,
NaiveDate,
Option<NaiveDate>,
)|
-> Result<_> {
let (id, name, slug, description, published_at, edited_at) =
p;
let tags = web::get_tags_by_post(id, db_conn)?;
Ok(PostWithoutContent {
name,
slug,
description,
published_at,
edited_at,
tags,
})
},
)
.collect::<Result<Vec<_>>>()?,
},
)
})
.collect::<Result<Vec<_>>>()?;
redis::cmd("SET")
.arg(keys::TAGS)
.arg(serde_json::to_string(&posts_by_tag)?)
.query(redis_conn)?;
redis::cmd("EXPIRE")
.arg(keys::TAGS)
.arg(config::CONFIG.cache_ttl)
.query(redis_conn)?;
Ok(posts_by_tag)
}
}
pub fn cache_tag_posts( pub fn cache_tag_posts(
id: i32, id: i32,
db_conn: &mut db::Connection, db_conn: &mut db::Connection,

View file

@ -8,19 +8,18 @@ use crate::{cache, db};
pub mod templates; pub mod templates;
use templates::TemplateToResponseWithStatusCode;
pub mod static_dir { pub mod static_dir {
include!(concat!(env!("OUT_DIR"), "/generated.rs")); include!(concat!(env!("OUT_DIR"), "/generated.rs"));
} }
async fn not_found() -> HttpResponse { async fn not_found() -> HttpResponse {
let mut resp = templates::StatusCode { templates::StatusCode {
status_code: http::StatusCode::NOT_FOUND, status_code: http::StatusCode::NOT_FOUND,
message: Some("maybe try a correct url?".to_string()), message: Some("maybe try a correct url?".to_string()),
} }
.to_response(); .to_response_with_status_code(http::StatusCode::NOT_FOUND)
*resp.status_mut() = http::StatusCode::NOT_FOUND;
resp
} }
#[get("/")] #[get("/")]
@ -89,7 +88,7 @@ async fn post_by_slug(
if let Some(post_id) = match db::schema::posts::table if let Some(post_id) = match db::schema::posts::table
.select(db::schema::posts::id) .select(db::schema::posts::id)
.filter(db::schema::posts::slug.eq(&slug)) .filter(db::schema::posts::slug.eq(slug))
.filter(db::schema::posts::active) .filter(db::schema::posts::active)
.first::<i32>(db_conn) .first::<i32>(db_conn)
.optional() .optional()
@ -104,125 +103,34 @@ async fn post_by_slug(
templates::PostBySlug { post }.to_response() templates::PostBySlug { post }.to_response()
} else { } else {
let mut resp = templates::StatusCode { templates::StatusCode {
status_code: http::StatusCode::NOT_FOUND, status_code: http::StatusCode::NOT_FOUND,
message: Some("this post does not exists... yet".to_string()), message: Some("this post does not exists... yet".to_string()),
} }
.to_response(); .to_response_with_status_code(http::StatusCode::NOT_FOUND)
*resp.status_mut() = http::StatusCode::NOT_FOUND; }
return resp;
} }
// let post_stripped: Option<(i32, String, NaiveDate, Option<NaiveDate>)> = #[get("/tags")]
// match db::schema::posts::table async fn tags(
// .select(( db_pool: web::Data<db::DbPool>,
// db::schema::posts::id, redis_pool: web::Data<cache::RedisPool>,
// db::schema::posts::name, ) -> HttpResponse {
// db::schema::posts::published_at, let db_conn = &mut match db_pool.get() {
// db::schema::posts::edited_at, Ok(x) => x,
// )) Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)),
// .filter(db::schema::posts::slug.eq(&slug)) };
// .filter(db::schema::posts::active) let redis_conn = &mut match redis_pool.get() {
// .first::<(i32, String, NaiveDate, Option<NaiveDate>)>(db_conn) Ok(x) => x,
// .optional() Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)),
// { };
// Ok(x) => x,
// Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)),
// };
//
// match post_stripped {
// Some(stripped) => {
// let (stripped_id, stripped_name, stripped_published_at, stripped_edited_at) = stripped;
// let key = cache::keys::post_content(stripped_id); let x = match cache::cache_tags(db_conn, redis_conn) {
Ok(x) => x,
Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)),
};
// match match redis::cmd("GET") templates::Tags { tags: x }.to_response()
// .arg(&key)
// .query::<Option<String>>(redis_conn.deref_mut())
// {
// Ok(x) => x,
// Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)),
// } {
// Some(s) => {
// let tags = match get_tags_by_post(stripped_id, db_conn) {
// Ok(x) => x,
// Err(e) => {
// return HttpResponse::InternalServerError().body(format!("{:?}", e))
// }
// };
// templates::PostBySlug {
// name: stripped_name,
// slug,
// published_at: stripped_published_at,
// edited_at: stripped_edited_at,
// tags,
// content: s,
// }
// }
// .to_response(),
// None => {
// let post = match db::schema::posts::table
// .filter(db::schema::posts::id.eq(stripped_id))
// .first::<db::models::Post>(db_conn)
// {
// Ok(x) => x,
// Err(e) => {
// return HttpResponse::InternalServerError().body(format!("{:?}", e))
// }
// };
// let html = markdown::to_html(&post.content);
// match redis::cmd("SET")
// .arg(&key)
// .arg(&html)
// .query::<Option<String>>(redis_conn.deref_mut())
// {
// Ok(x) => x,
// Err(e) => {
// return HttpResponse::InternalServerError().body(format!("{:?}", e))
// }
// };
// if let Err(e) = redis::cmd("EXPIRE")
// .arg(key)
// .arg(config::CONFIG.cache_ttl)
// .query::<()>(redis_conn.deref_mut())
// {
// return HttpResponse::InternalServerError().body(format!("{:?}", e));
// }
// let tags = match get_tags_by_post(stripped_id, db_conn) {
// Ok(x) => x,
// Err(e) => {
// return HttpResponse::InternalServerError().body(format!("{:?}", e))
// }
// };
// templates::PostBySlug {
// name: post.name,
// slug: post.slug,
// published_at: stripped_published_at,
// edited_at: stripped_edited_at,
// tags,
// content: html,
// }
// .to_response()
// }
// }
// }
// None => {
// let mut resp = templates::StatusCode {
// status_code: http::StatusCode::NOT_FOUND,
// message: Some("this post does not exists... yet".to_string()),
// }
// .to_response();
// *resp.status_mut() = http::StatusCode::NOT_FOUND;
// resp
// }
// }
} }
#[get("/tags/{name}")] #[get("/tags/{name}")]
@ -272,6 +180,7 @@ fn setup_routes(cfg: &mut web::ServiceConfig) {
.service(posts) .service(posts)
.service(ResourceFiles::new("/static", generated)) .service(ResourceFiles::new("/static", generated))
.service(post_by_slug) .service(post_by_slug)
.service(tags)
.service(tag_by_name) .service(tag_by_name)
.default_service(web::route().to(not_found)); .default_service(web::route().to(not_found));
} }

View file

@ -1,10 +1,44 @@
use actix_web::http; use actix_web::{body::BoxBody, http, HttpResponse, HttpResponseBuilder, ResponseError};
use askama_actix::Template; use askama_actix::Template;
use chrono::prelude::*; use chrono::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt;
use crate::cache; use crate::cache;
struct ActixError(askama::Error);
impl fmt::Debug for ActixError {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
<askama::Error as fmt::Debug>::fmt(&self.0, f)
}
}
impl fmt::Display for ActixError {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
<askama::Error as fmt::Display>::fmt(&self.0, f)
}
}
impl ResponseError for ActixError {}
pub trait TemplateToResponseWithStatusCode {
fn to_response_with_status_code(&self, code: http::StatusCode) -> HttpResponse<BoxBody>;
}
impl<T: askama_actix::Template> TemplateToResponseWithStatusCode for T {
fn to_response_with_status_code(&self, code: http::StatusCode) -> HttpResponse<BoxBody> {
match self.render() {
Ok(buffer) => HttpResponseBuilder::new(code)
.content_type(http::header::HeaderValue::from_static(T::MIME_TYPE))
.body(buffer),
Err(err) => HttpResponse::from_error(ActixError(err)),
}
}
}
#[derive(Template)] #[derive(Template)]
#[template(path = "status_code.html")] #[template(path = "status_code.html")]
pub struct StatusCode { pub struct StatusCode {
@ -39,13 +73,13 @@ pub struct Posts {
#[derive(Template)] #[derive(Template)]
#[template(path = "web/posts/{slug}.html")] #[template(path = "web/posts/{slug}.html")]
pub struct PostBySlug { pub struct PostBySlug {
// pub name: String, pub post: cache::PostWithoutDescription,
// pub slug: String, }
// pub published_at: NaiveDate,
// pub edited_at: Option<NaiveDate>, #[derive(Template)]
// pub tags: Vec<String>, #[template(path = "web/tags/index.html")]
// pub content: String, pub struct Tags {
pub post: cache::Post, pub tags: Vec<cache::PostsByTag>,
} }
#[derive(Template)] #[derive(Template)]

View file

@ -9,6 +9,7 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h1>Post archive</h1>
<ul class="post-index dashed"> <ul class="post-index dashed">
{% for post in posts %} {% for post in posts %}
<li> <li>

View file

@ -0,0 +1,36 @@
{% extends "base.html" %}
{% block title %}posts{% endblock %}
{% block head %}{% endblock %}
{% block breadcrumb %}
<li><a href="/posts">posts</a></li>
{% endblock %}
{% block content %}
<h1>Tags</h1>
{% for tag in tags %}
<div>
<h2>{{ tag.name }}</h2>
<ul class="post-index dashed">
{% for post in tag.posts %}
<li>
<span>
<a href="/posts/{{ post.slug }}">{{ post.name }}</a>
(<i>{{ post.published_at }}{% match post.edited_at %}{% when Some with (x) %} -> {{ x }}{% when None %}{% endmatch %}</i>)
</span>
<br />
<ul class="tag-list">
{% for tag in post.tags %}
<li><a href="/tags/{{ tag }}">{{ tag }}</a></li>
{% endfor %}
</ul>
<br />
<span>{{ post.description }}</span>
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
{% endblock %}