Initial version of pools
This commit is contained in:
parent
fe7edb93ad
commit
2b6b1f30f4
21 changed files with 577 additions and 36 deletions
|
|
@ -5,10 +5,11 @@ Sam's small image board. Currently a WIP.
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
- [ ] Pools
|
- [ ] Pools
|
||||||
|
- [ ] Config
|
||||||
- [ ] Video support
|
- [ ] Video support
|
||||||
- [ ] Cleanup/fixup background tasks
|
- [ ] Cleanup/fixup background tasks
|
||||||
- [ ] CSS
|
- [ ] CSS
|
||||||
- [ ] CLI, env vars, logging...
|
- [ ] Cleanup, CLI, env vars, logging, better errors...
|
||||||
|
|
||||||
## Running in development
|
## Running in development
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,18 @@ impl MigrationTrait for Migration {
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
manager
|
||||||
|
.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(SameyConfig::Table)
|
||||||
|
.if_not_exists()
|
||||||
|
.col(pk_auto(SameyConfig::Id))
|
||||||
|
.col(string_uniq(SameyConfig::Key))
|
||||||
|
.col(json(SameyConfig::Data))
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
manager
|
manager
|
||||||
.create_table(
|
.create_table(
|
||||||
Table::create()
|
Table::create()
|
||||||
|
|
@ -51,6 +63,15 @@ impl MigrationTrait for Migration {
|
||||||
.if_not_exists()
|
.if_not_exists()
|
||||||
.col(pk_auto(SameyPool::Id))
|
.col(pk_auto(SameyPool::Id))
|
||||||
.col(string_len_uniq(SameyPool::Name, 100))
|
.col(string_len_uniq(SameyPool::Name, 100))
|
||||||
|
.col(integer(SameyPool::UploaderId))
|
||||||
|
.col(boolean(SameyPool::IsPublic).default(false))
|
||||||
|
.foreign_key(
|
||||||
|
ForeignKeyCreateStatement::new()
|
||||||
|
.name("fk-samey_pool-samey_user-uploader_id")
|
||||||
|
.from(SameyPool::Table, SameyPool::UploaderId)
|
||||||
|
.to(SameyUser::Table, SameyUser::Id)
|
||||||
|
.on_delete(ForeignKeyAction::Cascade),
|
||||||
|
)
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
@ -220,6 +241,10 @@ impl MigrationTrait for Migration {
|
||||||
.drop_table(Table::drop().table(SameyUser::Table).to_owned())
|
.drop_table(Table::drop().table(SameyUser::Table).to_owned())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
manager
|
||||||
|
.drop_table(Table::drop().table(SameyConfig::Table).to_owned())
|
||||||
|
.await?;
|
||||||
|
|
||||||
manager
|
manager
|
||||||
.drop_table(Table::drop().table(SameySession::Table).to_owned())
|
.drop_table(Table::drop().table(SameySession::Table).to_owned())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
@ -238,6 +263,15 @@ enum SameySession {
|
||||||
ExpiryDate,
|
ExpiryDate,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(DeriveIden)]
|
||||||
|
enum SameyConfig {
|
||||||
|
#[sea_orm(iden = "samey_config")]
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
Key,
|
||||||
|
Data,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(DeriveIden)]
|
#[derive(DeriveIden)]
|
||||||
enum SameyUser {
|
enum SameyUser {
|
||||||
#[sea_orm(iden = "samey_user")]
|
#[sea_orm(iden = "samey_user")]
|
||||||
|
|
@ -314,6 +348,8 @@ enum SameyPool {
|
||||||
Table,
|
Table,
|
||||||
Id,
|
Id,
|
||||||
Name,
|
Name,
|
||||||
|
UploaderId,
|
||||||
|
IsPublic,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(DeriveIden)]
|
#[derive(DeriveIden)]
|
||||||
|
|
|
||||||
10
src/auth.rs
10
src/auth.rs
|
|
@ -133,7 +133,16 @@ impl SessionStore for SessionStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn save(&self, record: &Record) -> session_store::Result<()> {
|
async fn save(&self, record: &Record) -> session_store::Result<()> {
|
||||||
|
let session = SameySession::find()
|
||||||
|
.filter(samey_session::Column::SessionId.eq(record.id.to_string()))
|
||||||
|
.one(&self.db)
|
||||||
|
.await
|
||||||
|
.map_err(|_| session_store::Error::Backend("Failed to find session".into()))?
|
||||||
|
.ok_or(session_store::Error::Backend(
|
||||||
|
"No corresponding session found".into(),
|
||||||
|
))?;
|
||||||
SameySession::update(samey_session::ActiveModel {
|
SameySession::update(samey_session::ActiveModel {
|
||||||
|
id: Set(session.id),
|
||||||
data: Set(sea_orm::JsonValue::Object(
|
data: Set(sea_orm::JsonValue::Object(
|
||||||
record
|
record
|
||||||
.data
|
.data
|
||||||
|
|
@ -144,7 +153,6 @@ impl SessionStore for SessionStorage {
|
||||||
expiry_date: Set(record.expiry_date.unix_timestamp()),
|
expiry_date: Set(record.expiry_date.unix_timestamp()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.filter(samey_session::Column::SessionId.eq(record.id.to_string()))
|
|
||||||
.exec(&self.db)
|
.exec(&self.db)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| session_store::Error::Backend("Failed to update session".into()))?;
|
.map_err(|_| session_store::Error::Backend("Failed to update session".into()))?;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
pub mod prelude;
|
pub mod prelude;
|
||||||
|
|
||||||
|
pub mod samey_config;
|
||||||
pub mod samey_pool;
|
pub mod samey_pool;
|
||||||
pub mod samey_pool_post;
|
pub mod samey_pool_post;
|
||||||
pub mod samey_post;
|
pub mod samey_post;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.8
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.8
|
||||||
|
|
||||||
|
pub use super::samey_config::Entity as SameyConfig;
|
||||||
pub use super::samey_pool::Entity as SameyPool;
|
pub use super::samey_pool::Entity as SameyPool;
|
||||||
pub use super::samey_pool_post::Entity as SameyPoolPost;
|
pub use super::samey_pool_post::Entity as SameyPoolPost;
|
||||||
pub use super::samey_post::Entity as SameyPost;
|
pub use super::samey_post::Entity as SameyPost;
|
||||||
|
|
|
||||||
18
src/entities/samey_config.rs
Normal file
18
src/entities/samey_config.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.8
|
||||||
|
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||||
|
#[sea_orm(table_name = "samey_config")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key)]
|
||||||
|
pub id: i32,
|
||||||
|
#[sea_orm(unique)]
|
||||||
|
pub key: String,
|
||||||
|
pub data: Json,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
|
|
@ -9,12 +9,22 @@ pub struct Model {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
#[sea_orm(unique)]
|
#[sea_orm(unique)]
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
pub uploader_id: i32,
|
||||||
|
pub is_public: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
pub enum Relation {
|
pub enum Relation {
|
||||||
#[sea_orm(has_many = "super::samey_pool_post::Entity")]
|
#[sea_orm(has_many = "super::samey_pool_post::Entity")]
|
||||||
SameyPoolPost,
|
SameyPoolPost,
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to = "super::samey_user::Entity",
|
||||||
|
from = "Column::UploaderId",
|
||||||
|
to = "super::samey_user::Column::Id",
|
||||||
|
on_update = "NoAction",
|
||||||
|
on_delete = "Cascade"
|
||||||
|
)]
|
||||||
|
SameyUser,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Related<super::samey_pool_post::Entity> for Entity {
|
impl Related<super::samey_pool_post::Entity> for Entity {
|
||||||
|
|
@ -23,4 +33,10 @@ impl Related<super::samey_pool_post::Entity> for Entity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Related<super::samey_user::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::SameyUser.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,18 @@ pub struct Model {
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
pub enum Relation {
|
pub enum Relation {
|
||||||
|
#[sea_orm(has_many = "super::samey_pool::Entity")]
|
||||||
|
SameyPool,
|
||||||
#[sea_orm(has_many = "super::samey_post::Entity")]
|
#[sea_orm(has_many = "super::samey_post::Entity")]
|
||||||
SameyPost,
|
SameyPost,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Related<super::samey_pool::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::SameyPool.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Related<super::samey_post::Entity> for Entity {
|
impl Related<super::samey_post::Entity> for Entity {
|
||||||
fn to() -> RelationDef {
|
fn to() -> RelationDef {
|
||||||
Relation::SameyPost.def()
|
Relation::SameyPost.def()
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ pub enum SameyError {
|
||||||
Authentication(String),
|
Authentication(String),
|
||||||
#[error("Not allowed")]
|
#[error("Not allowed")]
|
||||||
Forbidden,
|
Forbidden,
|
||||||
|
#[error("Bad request: {0}")]
|
||||||
|
BadRequest(String),
|
||||||
#[error("Internal error: {0}")]
|
#[error("Internal error: {0}")]
|
||||||
Other(String),
|
Other(String),
|
||||||
}
|
}
|
||||||
|
|
@ -42,7 +44,7 @@ impl IntoResponse for SameyError {
|
||||||
| SameyError::Other(_) => {
|
| SameyError::Other(_) => {
|
||||||
(StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong!").into_response()
|
(StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong!").into_response()
|
||||||
}
|
}
|
||||||
SameyError::Multipart(_) => {
|
SameyError::Multipart(_) | SameyError::BadRequest(_) => {
|
||||||
(StatusCode::BAD_REQUEST, "Invalid request").into_response()
|
(StatusCode::BAD_REQUEST, "Invalid request").into_response()
|
||||||
}
|
}
|
||||||
SameyError::NotFound => (StatusCode::NOT_FOUND, "Resource not found").into_response(),
|
SameyError::NotFound => (StatusCode::NOT_FOUND, "Resource not found").into_response(),
|
||||||
|
|
|
||||||
42
src/lib.rs
42
src/lib.rs
|
|
@ -7,7 +7,6 @@ pub(crate) mod views;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use auth::SessionStorage;
|
|
||||||
use axum::{
|
use axum::{
|
||||||
Router,
|
Router,
|
||||||
extract::DefaultBodyLimit,
|
extract::DefaultBodyLimit,
|
||||||
|
|
@ -20,13 +19,14 @@ use tokio::fs;
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
use tower_sessions::SessionManagerLayer;
|
use tower_sessions::SessionManagerLayer;
|
||||||
|
|
||||||
use crate::auth::Backend;
|
use crate::auth::{Backend, SessionStorage};
|
||||||
use crate::entities::{prelude::SameyUser, samey_user};
|
use crate::entities::{prelude::SameyUser, samey_user};
|
||||||
pub use crate::error::SameyError;
|
pub use crate::error::SameyError;
|
||||||
use crate::views::{
|
use crate::views::{
|
||||||
add_post_source, delete_post, edit_post_details, get_full_media, get_media, index, login,
|
add_post_source, add_post_to_pool, change_pool_visibility, create_pool, delete_post,
|
||||||
logout, post_details, posts, posts_page, remove_field, search_tags, select_tag,
|
edit_post_details, get_full_media, get_media, get_pools, get_pools_page, index, login, logout,
|
||||||
submit_post_details, upload, view_post,
|
post_details, posts, posts_page, remove_field, remove_pool_post, search_tags, select_tag,
|
||||||
|
submit_post_details, upload, view_pool, view_post,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) const NEGATIVE_PREFIX: &str = "-";
|
pub(crate) const NEGATIVE_PREFIX: &str = "-";
|
||||||
|
|
@ -67,25 +67,39 @@ pub async fn get_router(db: DatabaseConnection, files_dir: &str) -> Result<Route
|
||||||
let auth_layer = AuthManagerLayerBuilder::new(Backend::new(db), session_layer).build();
|
let auth_layer = AuthManagerLayerBuilder::new(Backend::new(db), session_layer).build();
|
||||||
|
|
||||||
Ok(Router::new()
|
Ok(Router::new()
|
||||||
|
// Auth routes
|
||||||
.route("/login", post(login))
|
.route("/login", post(login))
|
||||||
.route("/logout", get(logout))
|
.route("/logout", get(logout))
|
||||||
|
// Tags routes
|
||||||
|
.route("/search_tags", post(search_tags))
|
||||||
|
.route("/select_tag", post(select_tag))
|
||||||
|
// Post routes
|
||||||
.route(
|
.route(
|
||||||
"/upload",
|
"/upload",
|
||||||
post(upload).layer(DefaultBodyLimit::max(100_000_000)),
|
post(upload).layer(DefaultBodyLimit::max(100_000_000)),
|
||||||
)
|
)
|
||||||
.route("/search_tags", post(search_tags))
|
.route("/post/{post_id}", get(view_post).delete(delete_post))
|
||||||
.route("/select_tag", post(select_tag))
|
|
||||||
.route("/posts", get(posts))
|
|
||||||
.route("/posts/{page}", get(posts_page))
|
|
||||||
.route("/view/{post_id}", get(view_post))
|
|
||||||
.route("/post/{post_id}", delete(delete_post))
|
|
||||||
.route("/post_details/{post_id}/edit", get(edit_post_details))
|
.route("/post_details/{post_id}/edit", get(edit_post_details))
|
||||||
.route("/post_details/{post_id}", get(post_details))
|
.route(
|
||||||
.route("/post_details/{post_id}", put(submit_post_details))
|
"/post_details/{post_id}",
|
||||||
|
get(post_details).put(submit_post_details),
|
||||||
|
)
|
||||||
.route("/post_source", post(add_post_source))
|
.route("/post_source", post(add_post_source))
|
||||||
.route("/remove", delete(remove_field))
|
|
||||||
.route("/media/{post_id}/full", get(get_full_media))
|
.route("/media/{post_id}/full", get(get_full_media))
|
||||||
.route("/media/{post_id}", get(get_media))
|
.route("/media/{post_id}", get(get_media))
|
||||||
|
// Pool routes
|
||||||
|
.route("/pools", get(get_pools))
|
||||||
|
.route("/pools/{page}", get(get_pools_page))
|
||||||
|
.route("/pool", post(create_pool))
|
||||||
|
.route("/pool/{pool_id}", get(view_pool))
|
||||||
|
.route("/pool/{pool_id}/public", put(change_pool_visibility))
|
||||||
|
.route("/pool/{pool_id}/post", post(add_post_to_pool))
|
||||||
|
.route("/pool_post/{pool_post_id}", delete(remove_pool_post))
|
||||||
|
// Search routes
|
||||||
|
.route("/posts", get(posts))
|
||||||
|
.route("/posts/{page}", get(posts_page))
|
||||||
|
// Other routes
|
||||||
|
.route("/remove", delete(remove_field))
|
||||||
.route("/", get(index))
|
.route("/", get(index))
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
.nest_service("/files", ServeDir::new(files_dir))
|
.nest_service("/files", ServeDir::new(files_dir))
|
||||||
|
|
|
||||||
47
src/query.rs
47
src/query.rs
|
|
@ -10,8 +10,8 @@ use crate::{
|
||||||
NEGATIVE_PREFIX, RATING_PREFIX,
|
NEGATIVE_PREFIX, RATING_PREFIX,
|
||||||
auth::User,
|
auth::User,
|
||||||
entities::{
|
entities::{
|
||||||
prelude::{SameyPost, SameyTag, SameyTagPost},
|
prelude::{SameyPoolPost, SameyPost, SameyTag, SameyTagPost},
|
||||||
samey_post, samey_tag, samey_tag_post,
|
samey_pool_post, samey_post, samey_tag, samey_tag_post,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -146,14 +146,53 @@ pub(crate) fn get_tags_for_post(post_id: i32) -> Select<SameyTag> {
|
||||||
.order_by_asc(samey_tag::Column::Name)
|
.order_by_asc(samey_tag::Column::Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, FromQueryResult)]
|
||||||
|
pub(crate) struct PoolPost {
|
||||||
|
pub(crate) id: i32,
|
||||||
|
pub(crate) thumbnail: String,
|
||||||
|
pub(crate) rating: String,
|
||||||
|
pub(crate) pool_post_id: i32,
|
||||||
|
pub(crate) position: f32,
|
||||||
|
pub(crate) tags: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_posts_in_pool(
|
||||||
|
pool_id: i32,
|
||||||
|
user: Option<&User>,
|
||||||
|
) -> Selector<SelectModel<PoolPost>> {
|
||||||
|
filter_by_user(
|
||||||
|
SameyPost::find()
|
||||||
|
.column(samey_post::Column::Id)
|
||||||
|
.column(samey_post::Column::Thumbnail)
|
||||||
|
.column(samey_post::Column::Rating)
|
||||||
|
.column_as(samey_pool_post::Column::Id, "pool_post_id")
|
||||||
|
.column(samey_pool_post::Column::Position)
|
||||||
|
.column_as(
|
||||||
|
Expr::cust("GROUP_CONCAT(\"samey_tag\".\"name\", ' ')"),
|
||||||
|
"tags",
|
||||||
|
)
|
||||||
|
.inner_join(SameyPoolPost)
|
||||||
|
.inner_join(SameyTagPost)
|
||||||
|
.join(
|
||||||
|
sea_orm::JoinType::InnerJoin,
|
||||||
|
samey_tag_post::Relation::SameyTag.def(),
|
||||||
|
)
|
||||||
|
.filter(samey_pool_post::Column::PoolId.eq(pool_id)),
|
||||||
|
user,
|
||||||
|
)
|
||||||
|
.group_by(samey_post::Column::Id)
|
||||||
|
.order_by_asc(samey_pool_post::Column::Position)
|
||||||
|
.into_model::<PoolPost>()
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn filter_by_user(query: Select<SameyPost>, user: Option<&User>) -> Select<SameyPost> {
|
pub(crate) fn filter_by_user(query: Select<SameyPost>, user: Option<&User>) -> Select<SameyPost> {
|
||||||
match user {
|
match user {
|
||||||
None => query.filter(samey_post::Column::IsPublic.into_simple_expr()),
|
None => query.filter(samey_post::Column::IsPublic.into_simple_expr()),
|
||||||
Some(user) if !user.is_admin => query.filter(
|
Some(user) if user.is_admin => query,
|
||||||
|
Some(user) => query.filter(
|
||||||
Condition::any()
|
Condition::any()
|
||||||
.add(samey_post::Column::IsPublic.into_simple_expr())
|
.add(samey_post::Column::IsPublic.into_simple_expr())
|
||||||
.add(samey_post::Column::UploaderId.eq(user.id)),
|
.add(samey_post::Column::UploaderId.eq(user.id)),
|
||||||
),
|
),
|
||||||
_ => query,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
272
src/views.rs
272
src/views.rs
|
|
@ -17,7 +17,8 @@ use itertools::Itertools;
|
||||||
use migration::{Expr, OnConflict};
|
use migration::{Expr, OnConflict};
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use sea_orm::{
|
use sea_orm::{
|
||||||
ActiveValue::Set, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QuerySelect,
|
ActiveValue::Set, ColumnTrait, Condition, EntityTrait, FromQueryResult, IntoSimpleExpr,
|
||||||
|
ModelTrait, PaginatorTrait, QueryFilter, QuerySelect,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tokio::task::spawn_blocking;
|
use tokio::task::spawn_blocking;
|
||||||
|
|
@ -26,11 +27,13 @@ use crate::{
|
||||||
AppState, NEGATIVE_PREFIX, RATING_PREFIX,
|
AppState, NEGATIVE_PREFIX, RATING_PREFIX,
|
||||||
auth::{AuthSession, Credentials, User},
|
auth::{AuthSession, Credentials, User},
|
||||||
entities::{
|
entities::{
|
||||||
prelude::{SameyPost, SameyPostSource, SameyTag, SameyTagPost},
|
prelude::{SameyPool, SameyPoolPost, SameyPost, SameyPostSource, SameyTag, SameyTagPost},
|
||||||
samey_post, samey_post_source, samey_tag, samey_tag_post,
|
samey_pool, samey_pool_post, samey_post, samey_post_source, samey_tag, samey_tag_post,
|
||||||
},
|
},
|
||||||
error::SameyError,
|
error::SameyError,
|
||||||
query::{PostOverview, filter_by_user, get_tags_for_post, search_posts},
|
query::{
|
||||||
|
PoolPost, PostOverview, filter_by_user, get_posts_in_pool, get_tags_for_post, search_posts,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAX_THUMBNAIL_DIMENSION: u32 = 192;
|
const MAX_THUMBNAIL_DIMENSION: u32 = 192;
|
||||||
|
|
@ -195,7 +198,6 @@ pub(crate) async fn upload(
|
||||||
thumbnail: Set(thumbnail_file),
|
thumbnail: Set(thumbnail_file),
|
||||||
title: Set(None),
|
title: Set(None),
|
||||||
description: Set(None),
|
description: Set(None),
|
||||||
is_public: Set(false),
|
|
||||||
rating: Set("u".to_owned()),
|
rating: Set("u".to_owned()),
|
||||||
uploaded_at: Set(Utc::now().naive_utc()),
|
uploaded_at: Set(Utc::now().naive_utc()),
|
||||||
parent_id: Set(None),
|
parent_id: Set(None),
|
||||||
|
|
@ -218,7 +220,7 @@ pub(crate) async fn upload(
|
||||||
.exec(&db)
|
.exec(&db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(Redirect::to(&format!("/view/{}", uploaded_post)))
|
Ok(Redirect::to(&format!("/post/{}", uploaded_post)))
|
||||||
} else {
|
} else {
|
||||||
Err(SameyError::Other("Missing parameters for upload".into()))
|
Err(SameyError::Other("Missing parameters for upload".into()))
|
||||||
}
|
}
|
||||||
|
|
@ -425,6 +427,264 @@ pub(crate) async fn posts_page(
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pool views
|
||||||
|
|
||||||
|
pub(crate) async fn get_pools(
|
||||||
|
state: State<AppState>,
|
||||||
|
auth_session: AuthSession,
|
||||||
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
|
get_pools_page(state, auth_session, Path(1)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "pools.html")]
|
||||||
|
struct GetPoolsTemplate {
|
||||||
|
pools: Vec<samey_pool::Model>,
|
||||||
|
page: u32,
|
||||||
|
page_count: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn get_pools_page(
|
||||||
|
State(AppState { db, .. }): State<AppState>,
|
||||||
|
auth_session: AuthSession,
|
||||||
|
Path(page): Path<u32>,
|
||||||
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
|
let query = match auth_session.user {
|
||||||
|
None => SameyPool::find().filter(samey_pool::Column::IsPublic.into_simple_expr()),
|
||||||
|
Some(user) if user.is_admin => SameyPool::find(),
|
||||||
|
Some(user) => SameyPool::find().filter(
|
||||||
|
Condition::any()
|
||||||
|
.add(samey_pool::Column::IsPublic.into_simple_expr())
|
||||||
|
.add(samey_pool::Column::UploaderId.eq(user.id)),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
let pagination = query.paginate(&db, 25);
|
||||||
|
let page_count = pagination.num_pages().await?;
|
||||||
|
|
||||||
|
let pools = pagination.fetch_page(page.saturating_sub(1) as u64).await?;
|
||||||
|
|
||||||
|
Ok(Html(
|
||||||
|
GetPoolsTemplate {
|
||||||
|
pools,
|
||||||
|
page,
|
||||||
|
page_count,
|
||||||
|
}
|
||||||
|
.render()?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(crate) struct CreatePoolForm {
|
||||||
|
pool: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn create_pool(
|
||||||
|
State(AppState { db, .. }): State<AppState>,
|
||||||
|
auth_session: AuthSession,
|
||||||
|
Form(body): Form<CreatePoolForm>,
|
||||||
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
|
let user = match auth_session.user {
|
||||||
|
Some(user) => user,
|
||||||
|
None => return Err(SameyError::Forbidden),
|
||||||
|
};
|
||||||
|
|
||||||
|
let pool_id = SameyPool::insert(samey_pool::ActiveModel {
|
||||||
|
name: Set(body.pool),
|
||||||
|
uploader_id: Set(user.id),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.exec(&db)
|
||||||
|
.await?
|
||||||
|
.last_insert_id;
|
||||||
|
|
||||||
|
Ok(Redirect::to(&format!("/pool/{}", pool_id)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "pool.html")]
|
||||||
|
struct ViewPoolTemplate {
|
||||||
|
pool: samey_pool::Model,
|
||||||
|
posts: Vec<PoolPost>,
|
||||||
|
can_edit: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn view_pool(
|
||||||
|
State(AppState { db, .. }): State<AppState>,
|
||||||
|
auth_session: AuthSession,
|
||||||
|
Path(pool_id): Path<u32>,
|
||||||
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
|
let pool_id = pool_id as i32;
|
||||||
|
let pool = SameyPool::find_by_id(pool_id)
|
||||||
|
.one(&db)
|
||||||
|
.await?
|
||||||
|
.ok_or(SameyError::NotFound)?;
|
||||||
|
|
||||||
|
let can_edit = match auth_session.user.as_ref() {
|
||||||
|
None => false,
|
||||||
|
Some(user) => user.is_admin || pool.uploader_id == user.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !pool.is_public && !can_edit {
|
||||||
|
return Err(SameyError::NotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
let posts = get_posts_in_pool(pool_id, auth_session.user.as_ref())
|
||||||
|
.all(&db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Html(
|
||||||
|
ViewPoolTemplate {
|
||||||
|
pool,
|
||||||
|
can_edit,
|
||||||
|
posts,
|
||||||
|
}
|
||||||
|
.render()?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(crate) struct ChangePoolVisibilityForm {
|
||||||
|
is_public: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn change_pool_visibility(
|
||||||
|
State(AppState { db, .. }): State<AppState>,
|
||||||
|
auth_session: AuthSession,
|
||||||
|
Path(pool_id): Path<u32>,
|
||||||
|
Form(body): Form<ChangePoolVisibilityForm>,
|
||||||
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
|
let pool_id = pool_id as i32;
|
||||||
|
let pool = SameyPool::find_by_id(pool_id)
|
||||||
|
.one(&db)
|
||||||
|
.await?
|
||||||
|
.expect("Pool for samey_pool_post must exist");
|
||||||
|
|
||||||
|
let can_edit = match auth_session.user.as_ref() {
|
||||||
|
None => false,
|
||||||
|
Some(user) => user.is_admin || pool.uploader_id == user.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !can_edit {
|
||||||
|
return Err(SameyError::Forbidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
SameyPool::update(samey_pool::ActiveModel {
|
||||||
|
id: Set(pool.id),
|
||||||
|
is_public: Set(body.is_public.is_some()),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.exec(&db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok("")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(crate) struct AddPostToPoolForm {
|
||||||
|
post_id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, FromQueryResult)]
|
||||||
|
pub(crate) struct PoolWithMaxPosition {
|
||||||
|
id: i32,
|
||||||
|
uploader_id: i32,
|
||||||
|
max_position: Option<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "add_post_to_pool.html")]
|
||||||
|
struct AddPostToPoolTemplate {
|
||||||
|
posts: Vec<PoolPost>,
|
||||||
|
can_edit: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn add_post_to_pool(
|
||||||
|
State(AppState { db, .. }): State<AppState>,
|
||||||
|
auth_session: AuthSession,
|
||||||
|
Path(pool_id): Path<u32>,
|
||||||
|
Form(body): Form<AddPostToPoolForm>,
|
||||||
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
|
let pool = SameyPool::find_by_id(pool_id as i32)
|
||||||
|
.select_only()
|
||||||
|
.column(samey_pool::Column::Id)
|
||||||
|
.column(samey_pool::Column::UploaderId)
|
||||||
|
.column_as(samey_pool_post::Column::Position.max(), "max_position")
|
||||||
|
.left_join(SameyPoolPost)
|
||||||
|
.group_by(samey_pool::Column::Id)
|
||||||
|
.into_model::<PoolWithMaxPosition>()
|
||||||
|
.one(&db)
|
||||||
|
.await?
|
||||||
|
.ok_or(SameyError::NotFound)?;
|
||||||
|
|
||||||
|
let can_edit_pool = match auth_session.user.as_ref() {
|
||||||
|
None => false,
|
||||||
|
Some(user) => user.is_admin || pool.uploader_id == user.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !can_edit_pool {
|
||||||
|
return Err(SameyError::Forbidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
let post = filter_by_user(
|
||||||
|
SameyPost::find_by_id(body.post_id),
|
||||||
|
auth_session.user.as_ref(),
|
||||||
|
)
|
||||||
|
.one(&db)
|
||||||
|
.await?
|
||||||
|
.ok_or(SameyError::NotFound)?;
|
||||||
|
|
||||||
|
SameyPoolPost::insert(samey_pool_post::ActiveModel {
|
||||||
|
pool_id: Set(pool.id),
|
||||||
|
post_id: Set(post.id),
|
||||||
|
position: Set(pool.max_position.unwrap_or(-1.0).floor() + 1.0),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.exec(&db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let posts = get_posts_in_pool(pool.id, auth_session.user.as_ref())
|
||||||
|
.all(&db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Html(
|
||||||
|
AddPostToPoolTemplate {
|
||||||
|
posts,
|
||||||
|
can_edit: true,
|
||||||
|
}
|
||||||
|
.render()?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn remove_pool_post(
|
||||||
|
State(AppState { db, .. }): State<AppState>,
|
||||||
|
auth_session: AuthSession,
|
||||||
|
Path(pool_post_id): Path<u32>,
|
||||||
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
|
let pool_post_id = pool_post_id as i32;
|
||||||
|
let pool_post = SameyPoolPost::find_by_id(pool_post_id)
|
||||||
|
.one(&db)
|
||||||
|
.await?
|
||||||
|
.ok_or(SameyError::NotFound)?;
|
||||||
|
let pool = SameyPool::find_by_id(pool_post.pool_id)
|
||||||
|
.one(&db)
|
||||||
|
.await?
|
||||||
|
.expect("Pool for samey_pool_post must exist");
|
||||||
|
|
||||||
|
let can_edit = match auth_session.user.as_ref() {
|
||||||
|
None => false,
|
||||||
|
Some(user) => user.is_admin || pool.uploader_id == user.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !can_edit {
|
||||||
|
return Err(SameyError::Forbidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
pool_post.delete(&db).await?;
|
||||||
|
|
||||||
|
Ok("")
|
||||||
|
}
|
||||||
|
|
||||||
// Single post views
|
// Single post views
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
|
|
|
||||||
9
templates/add_post_to_pool.html
Normal file
9
templates/add_post_to_pool.html
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{% include "pool_posts.html" %}
|
||||||
|
<input
|
||||||
|
id="add-post-input"
|
||||||
|
name="post_id"
|
||||||
|
type="text"
|
||||||
|
pattern="[0-9]*"
|
||||||
|
hx-swap-oob="outerHTML"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
<textarea name="description">{% if let Some(description) = post.description %}{{ description }}{% endif %}</textarea>
|
<textarea name="description">{% if let Some(description) = post.description %}{{ description }}{% endif %}</textarea>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>Is public?</label>
|
<label>Is public post?</label>
|
||||||
<input name="is_public" type="checkbox" {% if post.is_public %}checked{% endif %} value="true" />
|
<input name="is_public" type="checkbox" {% if post.is_public %}checked{% endif %} value="true" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>Parent post</label>
|
<label>Parent post</label>
|
||||||
<input name="parent_post" type="text" value="{% if let Some(parent_id) = post.parent_id %}{{ parent_id }}{% endif %}" />
|
<input name="parent_post" type="text" pattern="[0-9]*" value="{% if let Some(parent_id) = post.parent_id %}{{ parent_id }}{% endif %}" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button>Submit</button>
|
<button>Submit</button>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
|
<h1>Samey</h1>
|
||||||
<article>
|
<article>
|
||||||
<h2>Search</h2>
|
<h2>Search</h2>
|
||||||
<form method="get" action="/posts/1">
|
<form method="get" action="/posts/1">
|
||||||
|
|
@ -27,6 +28,10 @@
|
||||||
<button type="submit">Search</button>
|
<button type="submit">Search</button>
|
||||||
</form>
|
</form>
|
||||||
</article>
|
</article>
|
||||||
|
<article>
|
||||||
|
<a href="/posts/1">Posts</a>
|
||||||
|
<a href="/pools/1">Pools</a>
|
||||||
|
</article>
|
||||||
{% if let Some(user) = user %}
|
{% if let Some(user) = user %}
|
||||||
<article>
|
<article>
|
||||||
<h2>Upload media</h2>
|
<h2>Upload media</h2>
|
||||||
|
|
@ -37,8 +42,9 @@
|
||||||
id="upload-tags"
|
id="upload-tags"
|
||||||
name="tags"
|
name="tags"
|
||||||
hx-post="/search_tags"
|
hx-post="/search_tags"
|
||||||
hx-trigger="input changed delay:400ms"
|
hx-trigger="input changed"
|
||||||
hx-target="next .tags-autocomplete"
|
hx-target="next .tags-autocomplete"
|
||||||
|
hx-vals="js:{selection_end: event.target.selectionEnd}"
|
||||||
hx-on::after-settle="this.focus(); this.setSelectionRange(-1, -1);"
|
hx-on::after-settle="this.focus(); this.setSelectionRange(-1, -1);"
|
||||||
/>
|
/>
|
||||||
<ul class="tags-autocomplete" id="upload-autocomplete"></ul>
|
<ul class="tags-autocomplete" id="upload-autocomplete"></ul>
|
||||||
|
|
@ -54,7 +60,13 @@
|
||||||
<article>
|
<article>
|
||||||
<h2>Create pool</h2>
|
<h2>Create pool</h2>
|
||||||
<form method="post" action="/pool">
|
<form method="post" action="/pool">
|
||||||
<input class="tags" type="text" id="pool-name" name="name" />
|
<input
|
||||||
|
class="pool"
|
||||||
|
type="text"
|
||||||
|
id="pool"
|
||||||
|
name="pool"
|
||||||
|
maxlength="100"
|
||||||
|
/>
|
||||||
<button type="submit">Submit</button>
|
<button type="submit">Submit</button>
|
||||||
</form>
|
</form>
|
||||||
</article>
|
</article>
|
||||||
|
|
|
||||||
65
templates/pool.html
Normal file
65
templates/pool.html
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||||
|
<script src=" https://cdn.jsdelivr.net/npm/sortablejs@1.15.6/Sortable.min.js "></script>
|
||||||
|
<title>Pool - {{ pool.name }} - Samey</title>
|
||||||
|
<meta property="og:title" content="{{ pool.name }}" />
|
||||||
|
<meta property="og:url" content="/pool/{{ pool.id }}" />
|
||||||
|
<meta property="twitter:title" content="{{ pool.name }}" />
|
||||||
|
<meta
|
||||||
|
property="og:description"
|
||||||
|
content="Pool with {{ posts.len() }} post(s)."
|
||||||
|
/>
|
||||||
|
{% if let Some(post) = posts.first() %}
|
||||||
|
<meta property="og:image" content="/files/{{ post.thumbnail }}" />
|
||||||
|
<meta property="twitter:image:src" content="/files/{{ post.thumbnail }}" />
|
||||||
|
{% endif %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<h1>Pool - {{ pool.name }}</h1>
|
||||||
|
</main>
|
||||||
|
<article>
|
||||||
|
<h2>Posts</h2>
|
||||||
|
{% include "pool_posts.html" %}
|
||||||
|
</article>
|
||||||
|
{% if can_edit %}
|
||||||
|
<article>
|
||||||
|
<form
|
||||||
|
hx-post="/pool/{{ pool.id }}/post"
|
||||||
|
hx-target="#pool-posts"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<label>Add post</label>
|
||||||
|
<input
|
||||||
|
id="add-post-input"
|
||||||
|
name="post_id"
|
||||||
|
type="text"
|
||||||
|
pattern="[0-9]*"
|
||||||
|
/>
|
||||||
|
<button>Add</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div>
|
||||||
|
<label>Is public pool?</label>
|
||||||
|
<input
|
||||||
|
name="is_public"
|
||||||
|
type="checkbox"
|
||||||
|
hx-put="/pool/{{ pool.id }}/public"
|
||||||
|
{%
|
||||||
|
if
|
||||||
|
pool.is_public
|
||||||
|
%}checked{%
|
||||||
|
endif
|
||||||
|
%}
|
||||||
|
value="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
{% endif %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
25
templates/pool_posts.html
Normal file
25
templates/pool_posts.html
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<div id="pool-posts">
|
||||||
|
{% if posts.is_empty() %}
|
||||||
|
<span>No posts in pool.</span>
|
||||||
|
{% else %}
|
||||||
|
<ul>
|
||||||
|
{% for post in posts %}
|
||||||
|
<li class="pool-post" data-position="{{ post.position }}">
|
||||||
|
<a href="/post/{{ post.id }}" title="{{ post.tags }}">
|
||||||
|
<img src="/files/{{ post.thumbnail }}" />
|
||||||
|
<div>{{ post.rating | upper }}</div>
|
||||||
|
</a>
|
||||||
|
{% if can_edit %}
|
||||||
|
<button
|
||||||
|
hx-delete="/pool_post/{{ post.pool_post_id }}"
|
||||||
|
hx-target="closest .pool-post"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
26
templates/pools.html
Normal file
26
templates/pools.html
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||||
|
<title>Pools - Samey</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<h1>Viewing pools</h1>
|
||||||
|
{% if pools.is_empty() %}
|
||||||
|
<div>No pools found!</div>
|
||||||
|
{% else %}
|
||||||
|
<ul>
|
||||||
|
{% for pool in pools %}
|
||||||
|
<li>
|
||||||
|
<a href="/pool/{{ pool.id }}"> {{ pool.name }} </a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<div>Page {{ page }} of {{ page_count }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -43,7 +43,7 @@
|
||||||
{% for post in posts %}
|
{% for post in posts %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="{% if let Some(tags_text) = tags_text %}/view/{{ post.id }}?tags={{ tags_text.replace(' ', "+") }}{% else %}/view/{{ post.id }}{% endif %}"
|
href="{% if let Some(tags_text) = tags_text %}/post/{{ post.id }}?tags={{ tags_text.replace(' ', "+") }}{% else %}/post/{{ post.id }}{% endif %}"
|
||||||
title="{{ post.tags }}"
|
title="{{ post.tags }}"
|
||||||
>
|
>
|
||||||
<img src="/files/{{ post.thumbnail }}" />
|
<img src="/files/{{ post.thumbnail }}" />
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{% include "post_details.html" %} {% if let Some(parent_post) = parent_post %}
|
{% include "post_details.html" %} {% if let Some(parent_post) = parent_post %}
|
||||||
<article id="parent-post" hx-swap-oob="outerHTML">
|
<article id="parent-post" hx-swap-oob="outerHTML">
|
||||||
<h2>Parent</h2>
|
<h2>Parent</h2>
|
||||||
<a href="/view/{{ parent_post.id }}" title="{{ parent_post.tags }}">
|
<a href="/post/{{ parent_post.id }}" title="{{ parent_post.tags }}">
|
||||||
<img src="/files/{{ parent_post.thumbnail }}" />
|
<img src="/files/{{ parent_post.thumbnail }}" />
|
||||||
<div>{{ parent_post.rating }}</div>
|
<div>{{ parent_post.rating }}</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
property="og:title"
|
property="og:title"
|
||||||
content="{% if let Some(title) = post.title %}{{ title }}{% endif %}"
|
content="{% if let Some(title) = post.title %}{{ title }}{% endif %}"
|
||||||
/>
|
/>
|
||||||
<meta property="og:url" content="/view/{{ post.id }}" />
|
<meta property="og:url" content="/post/{{ post.id }}" />
|
||||||
<meta property="og:image" content="/files/{{ post.media }}" />
|
<meta property="og:image" content="/files/{{ post.media }}" />
|
||||||
<meta property="og:image:width" content="{{ post.width }}" />
|
<meta property="og:image:width" content="{{ post.width }}" />
|
||||||
<meta property="og:image:height" content="{{ post.height }}" />
|
<meta property="og:image:height" content="{{ post.height }}" />
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
{% if let Some(parent_post) = parent_post %}
|
{% if let Some(parent_post) = parent_post %}
|
||||||
<article id="parent-post">
|
<article id="parent-post">
|
||||||
<h2>Parent</h2>
|
<h2>Parent</h2>
|
||||||
<a href="/view/{{ parent_post.id }}" title="{{ parent_post.tags }}">
|
<a href="/post/{{ parent_post.id }}" title="{{ parent_post.tags }}">
|
||||||
<img src="/files/{{ parent_post.thumbnail }}" />
|
<img src="/files/{{ parent_post.thumbnail }}" />
|
||||||
<div>{{ parent_post.rating }}</div>
|
<div>{{ parent_post.rating }}</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -50,7 +50,7 @@
|
||||||
<ul>
|
<ul>
|
||||||
{% for child_post in children_posts %}
|
{% for child_post in children_posts %}
|
||||||
<li>
|
<li>
|
||||||
<a href="/view/{{ child_post.id }}" title="{{ child_post.tags }}">
|
<a href="/post/{{ child_post.id }}" title="{{ child_post.tags }}">
|
||||||
<img src="/files/{{ child_post.thumbnail }}" />
|
<img src="/files/{{ child_post.thumbnail }}" />
|
||||||
<div>{{ child_post.rating | upper }}</div>
|
<div>{{ child_post.rating | upper }}</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue