diff --git a/README.md b/README.md index 742b690..df39807 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ Sam's small image board. Currently a WIP. ## TODO -- [ ] Improve filters - [ ] Pools - [ ] Video support - [ ] CSS diff --git a/src/lib.rs b/src/lib.rs index d06401c..a73bcb5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,6 +29,9 @@ use crate::views::{ submit_post_details, upload, view_post, }; +pub(crate) const NEGATIVE_PREFIX: &str = "-"; +pub(crate) const RATING_PREFIX: &str = "rating:"; + #[derive(Clone)] pub(crate) struct AppState { files_dir: Arc, diff --git a/src/query.rs b/src/query.rs index d3cc552..2f81278 100644 --- a/src/query.rs +++ b/src/query.rs @@ -7,6 +7,7 @@ use sea_orm::{ }; use crate::{ + NEGATIVE_PREFIX, RATING_PREFIX, auth::User, entities::{ prelude::{SameyPost, SameyTag, SameyTagPost}, @@ -25,64 +26,110 @@ pub(crate) fn search_posts( tags: Option<&Vec<&str>>, user: Option, ) -> Selector> { - let tags: HashSet = match tags { - Some(tags) if !tags.is_empty() => tags.iter().map(|&tag| tag.to_lowercase()).collect(), - _ => HashSet::new(), + let mut include_tags = HashSet::::new(); + let mut exclude_tags = HashSet::::new(); + let mut include_ratings = HashSet::::new(); + let mut exclude_ratings = HashSet::::new(); + if let Some(tags) = tags { + for mut tag in tags.iter().map(|tag| tag.to_lowercase()) { + if tag.starts_with(NEGATIVE_PREFIX) { + if tag.as_str()[NEGATIVE_PREFIX.len()..].starts_with(RATING_PREFIX) { + exclude_ratings + .insert(tag.split_off(NEGATIVE_PREFIX.len() + RATING_PREFIX.len())); + } else { + exclude_tags.insert(tag.split_off(NEGATIVE_PREFIX.len())); + } + } else if tag.starts_with(RATING_PREFIX) { + include_ratings.insert(tag.split_off(RATING_PREFIX.len())); + } else { + include_tags.insert(tag); + } + } + } + + let mut query = if include_tags.is_empty() && exclude_tags.is_empty() { + let mut query = SameyPost::find() + .select_only() + .column(samey_post::Column::Id) + .column(samey_post::Column::Thumbnail) + .column_as( + Expr::cust("GROUP_CONCAT(\"samey_tag\".\"name\", ' ')"), + "tags", + ) + .inner_join(SameyTagPost) + .join( + sea_orm::JoinType::InnerJoin, + samey_tag_post::Relation::SameyTag.def(), + ); + if !include_ratings.is_empty() { + query = query.filter(samey_post::Column::Rating.is_in(include_ratings)) + } + if !exclude_ratings.is_empty() { + query = query.filter(samey_post::Column::Rating.is_not_in(exclude_ratings)) + } + query + } else { + let mut query = SameyPost::find() + .select_only() + .column(samey_post::Column::Id) + .column(samey_post::Column::Thumbnail) + .column_as( + Expr::cust("GROUP_CONCAT(\"samey_tag\".\"name\", ' ')"), + "tags", + ) + .inner_join(SameyTagPost) + .join( + sea_orm::JoinType::InnerJoin, + samey_tag_post::Relation::SameyTag.def(), + ); + if !include_tags.is_empty() { + let include_tags_count = include_tags.len() as u32; + let include_tags_subquery = Query::select() + .column((SameyPost, samey_post::Column::Id)) + .from(SameyPost) + .inner_join( + SameyTagPost, + Expr::col((SameyPost, samey_post::Column::Id)) + .equals((SameyTagPost, samey_tag_post::Column::PostId)), + ) + .inner_join( + SameyTag, + Expr::col((SameyTagPost, samey_tag_post::Column::TagId)) + .equals((SameyTag, samey_tag::Column::Id)), + ) + .and_where(samey_tag::Column::NormalizedName.is_in(include_tags)) + .group_by_col((SameyPost, samey_post::Column::Id)) + .and_having(samey_tag::Column::Id.count().eq(include_tags_count)) + .to_owned(); + query = query.filter(samey_post::Column::Id.in_subquery(include_tags_subquery)); + } + if !exclude_tags.is_empty() { + let exclude_tags_subquery = Query::select() + .column((SameyPost, samey_post::Column::Id)) + .from(SameyPost) + .inner_join( + SameyTagPost, + Expr::col((SameyPost, samey_post::Column::Id)) + .equals((SameyTagPost, samey_tag_post::Column::PostId)), + ) + .inner_join( + SameyTag, + Expr::col((SameyTagPost, samey_tag_post::Column::TagId)) + .equals((SameyTag, samey_tag::Column::Id)), + ) + .and_where(samey_tag::Column::NormalizedName.is_in(exclude_tags)) + .to_owned(); + query = query.filter(samey_post::Column::Id.not_in_subquery(exclude_tags_subquery)); + } + if !include_ratings.is_empty() { + query = query.filter(samey_post::Column::Rating.is_in(include_ratings)) + } + if !exclude_ratings.is_empty() { + query = query.filter(samey_post::Column::Rating.is_not_in(exclude_ratings)) + } + query }; - let mut query = if tags.is_empty() { - SameyPost::find() - .select_only() - .column(samey_post::Column::Id) - .column(samey_post::Column::Thumbnail) - .column_as( - Expr::cust("GROUP_CONCAT(\"samey_tag\".\"name\", ' ')"), - "tags", - ) - .inner_join(SameyTagPost) - .join( - sea_orm::JoinType::InnerJoin, - samey_tag_post::Relation::SameyTag.def(), - ) - .group_by(samey_post::Column::Id) - .order_by_desc(samey_post::Column::Id) - } else { - let tags_count = tags.len() as u32; - let subquery = Query::select() - .column((SameyPost, samey_post::Column::Id)) - .from(SameyPost) - .inner_join( - SameyTagPost, - Expr::col((SameyPost, samey_post::Column::Id)) - .equals((SameyTagPost, samey_tag_post::Column::PostId)), - ) - .inner_join( - SameyTag, - Expr::col((SameyTagPost, samey_tag_post::Column::TagId)) - .equals((SameyTag, samey_tag::Column::Id)), - ) - .and_where(samey_tag::Column::NormalizedName.is_in(tags)) - .group_by_col((SameyPost, samey_post::Column::Id)) - .and_having(samey_tag::Column::Id.count().eq(tags_count)) - .to_owned(); - SameyPost::find() - .select_only() - .column(samey_post::Column::Id) - .column(samey_post::Column::Thumbnail) - .column_as( - Expr::cust("GROUP_CONCAT(\"samey_tag\".\"name\", ' ')"), - "tags", - ) - .inner_join(SameyTagPost) - .join( - sea_orm::JoinType::InnerJoin, - samey_tag_post::Relation::SameyTag.def(), - ) - .filter(samey_post::Column::Id.in_subquery(subquery)) - .group_by(samey_post::Column::Id) - .order_by_desc(samey_post::Column::Id) - // println!("{}", &query.build(sea_orm::DatabaseBackend::Sqlite).sql); - }; query = match user { None => query.filter(samey_post::Column::IsPublic.into_simple_expr()), Some(user) if !user.is_admin => query.filter( @@ -93,7 +140,10 @@ pub(crate) fn search_posts( _ => query, }; - query.into_model::() + query + .group_by(samey_post::Column::Id) + .order_by_desc(samey_post::Column::Id) + .into_model::() } pub(crate) fn get_tags_for_post(post_id: i32) -> Select { diff --git a/src/views.rs b/src/views.rs index 995f047..6fc74dd 100644 --- a/src/views.rs +++ b/src/views.rs @@ -23,7 +23,7 @@ use serde::Deserialize; use tokio::task::spawn_blocking; use crate::{ - AppState, + AppState, NEGATIVE_PREFIX, RATING_PREFIX, auth::{AuthSession, Credentials, User}, entities::{ prelude::{SameyPost, SameyPostSource, SameyTag, SameyTagPost}, @@ -226,10 +226,15 @@ pub(crate) async fn upload( // Search fields views +struct SearchTag { + name: String, + value: String, +} + #[derive(Template)] #[template(path = "search_tags.html")] struct SearchTagsTemplate { - tags: Vec, + tags: Vec, } #[derive(Debug, Deserialize)] @@ -242,14 +247,66 @@ pub(crate) async fn search_tags( Form(body): Form, ) -> Result { let tags = match body.tags.split(' ').last() { - Some(tag) if !tag.is_empty() => { - SameyTag::find() - .filter(Expr::cust_with_expr( - "LOWER(\"samey_tag\".\"name\") LIKE CONCAT(?, '%')", - tag.to_lowercase(), - )) - .all(&db) - .await? + Some(tag) => { + if tag.starts_with(NEGATIVE_PREFIX) { + if tag[NEGATIVE_PREFIX.len()..].starts_with(RATING_PREFIX) { + [ + format!("{}u", RATING_PREFIX), + format!("{}s", RATING_PREFIX), + format!("{}q", RATING_PREFIX), + format!("{}e", RATING_PREFIX), + ] + .into_iter() + .filter(|t| t.starts_with(&tag[NEGATIVE_PREFIX.len()..])) + .map(|tag| SearchTag { + value: format!("-{}", &tag), + name: tag, + }) + .collect() + } else { + SameyTag::find() + .filter(Expr::cust_with_expr( + "LOWER(\"samey_tag\".\"name\") LIKE CONCAT(?, '%')", + tag[NEGATIVE_PREFIX.len()..].to_lowercase(), + )) + .all(&db) + .await? + .into_iter() + .map(|tag| SearchTag { + value: format!("-{}", &tag.name), + name: tag.name, + }) + .collect() + } + } else if tag.starts_with(RATING_PREFIX) { + [ + format!("{}u", RATING_PREFIX), + format!("{}s", RATING_PREFIX), + format!("{}q", RATING_PREFIX), + format!("{}e", RATING_PREFIX), + ] + .into_iter() + .filter(|t| t.starts_with(tag)) + .map(|tag| SearchTag { + value: tag.clone(), + name: tag, + }) + .collect() + } else { + SameyTag::find() + .filter(Expr::cust_with_expr( + "LOWER(\"samey_tag\".\"name\") LIKE CONCAT(?, '%')", + tag.to_lowercase(), + )) + .all(&db) + .await? + .into_iter() + .map(|tag| SearchTag { + value: tag.name.clone(), + name: tag.name, + }) + .collect() + } } _ => vec![], }; diff --git a/templates/posts.html b/templates/posts.html index 03a0852..efe035d 100644 --- a/templates/posts.html +++ b/templates/posts.html @@ -35,6 +35,9 @@

Viewing posts

+ {% if posts.is_empty() %} +
No posts found!
+ {% else %}
    {% for post in posts %}
  • @@ -48,6 +51,7 @@ {% endfor %}
Page {{ page }} of {{ page_count }}
+ {% endif %}
diff --git a/templates/search_tags.html b/templates/search_tags.html index 0580a75..79f4b63 100644 --- a/templates/search_tags.html +++ b/templates/search_tags.html @@ -1,7 +1,7 @@ {% for tag in tags %}