Update
This commit is contained in:
parent
964534d0d9
commit
e262fc8ab2
6 changed files with 213 additions and 139 deletions
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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>
|
||||||
|
|
36
backend/templates/web/tags/index.html
Normal file
36
backend/templates/web/tags/index.html
Normal 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 %}
|
Loading…
Reference in a new issue