This commit is contained in:
Bad Manners 2025-04-08 20:07:21 -03:00
parent a5e3fb2850
commit 04f888c323
6 changed files with 182 additions and 69 deletions

View file

@ -4,7 +4,6 @@ Sam's small image board. Currently a WIP.
## TODO ## TODO
- [ ] Improve filters
- [ ] Pools - [ ] Pools
- [ ] Video support - [ ] Video support
- [ ] CSS - [ ] CSS

View file

@ -29,6 +29,9 @@ use crate::views::{
submit_post_details, upload, view_post, submit_post_details, upload, view_post,
}; };
pub(crate) const NEGATIVE_PREFIX: &str = "-";
pub(crate) const RATING_PREFIX: &str = "rating:";
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct AppState { pub(crate) struct AppState {
files_dir: Arc<String>, files_dir: Arc<String>,

View file

@ -7,6 +7,7 @@ use sea_orm::{
}; };
use crate::{ use crate::{
NEGATIVE_PREFIX, RATING_PREFIX,
auth::User, auth::User,
entities::{ entities::{
prelude::{SameyPost, SameyTag, SameyTagPost}, prelude::{SameyPost, SameyTag, SameyTagPost},
@ -25,64 +26,110 @@ pub(crate) fn search_posts(
tags: Option<&Vec<&str>>, tags: Option<&Vec<&str>>,
user: Option<User>, user: Option<User>,
) -> Selector<SelectModel<SearchPost>> { ) -> Selector<SelectModel<SearchPost>> {
let tags: HashSet<String> = match tags { let mut include_tags = HashSet::<String>::new();
Some(tags) if !tags.is_empty() => tags.iter().map(|&tag| tag.to_lowercase()).collect(), let mut exclude_tags = HashSet::<String>::new();
_ => HashSet::new(), let mut include_ratings = HashSet::<String>::new();
let mut exclude_ratings = HashSet::<String>::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 { query = 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.filter(
@ -93,7 +140,10 @@ pub(crate) fn search_posts(
_ => query, _ => query,
}; };
query.into_model::<SearchPost>() query
.group_by(samey_post::Column::Id)
.order_by_desc(samey_post::Column::Id)
.into_model::<SearchPost>()
} }
pub(crate) fn get_tags_for_post(post_id: i32) -> Select<SameyTag> { pub(crate) fn get_tags_for_post(post_id: i32) -> Select<SameyTag> {

View file

@ -23,7 +23,7 @@ use serde::Deserialize;
use tokio::task::spawn_blocking; use tokio::task::spawn_blocking;
use crate::{ use crate::{
AppState, AppState, NEGATIVE_PREFIX, RATING_PREFIX,
auth::{AuthSession, Credentials, User}, auth::{AuthSession, Credentials, User},
entities::{ entities::{
prelude::{SameyPost, SameyPostSource, SameyTag, SameyTagPost}, prelude::{SameyPost, SameyPostSource, SameyTag, SameyTagPost},
@ -226,10 +226,15 @@ pub(crate) async fn upload(
// Search fields views // Search fields views
struct SearchTag {
name: String,
value: String,
}
#[derive(Template)] #[derive(Template)]
#[template(path = "search_tags.html")] #[template(path = "search_tags.html")]
struct SearchTagsTemplate { struct SearchTagsTemplate {
tags: Vec<samey_tag::Model>, tags: Vec<SearchTag>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -242,14 +247,66 @@ pub(crate) async fn search_tags(
Form(body): Form<SearchTagsForm>, Form(body): Form<SearchTagsForm>,
) -> Result<impl IntoResponse, SameyError> { ) -> Result<impl IntoResponse, SameyError> {
let tags = match body.tags.split(' ').last() { let tags = match body.tags.split(' ').last() {
Some(tag) if !tag.is_empty() => { Some(tag) => {
SameyTag::find() if tag.starts_with(NEGATIVE_PREFIX) {
.filter(Expr::cust_with_expr( if tag[NEGATIVE_PREFIX.len()..].starts_with(RATING_PREFIX) {
"LOWER(\"samey_tag\".\"name\") LIKE CONCAT(?, '%')", [
tag.to_lowercase(), format!("{}u", RATING_PREFIX),
)) format!("{}s", RATING_PREFIX),
.all(&db) format!("{}q", RATING_PREFIX),
.await? 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![], _ => vec![],
}; };

View file

@ -35,6 +35,9 @@
</article> </article>
<main> <main>
<h1>Viewing posts</h1> <h1>Viewing posts</h1>
{% if posts.is_empty() %}
<div>No posts found!</div>
{% else %}
<ul> <ul>
{% for post in posts %} {% for post in posts %}
<li> <li>
@ -48,6 +51,7 @@
{% endfor %} {% endfor %}
</ul> </ul>
<div>Page {{ page }} of {{ page_count }}</div> <div>Page {{ page }} of {{ page_count }}</div>
{% endif %}
</main> </main>
</body> </body>
</html> </html>

View file

@ -1,7 +1,7 @@
{% for tag in tags %} {% for tag in tags %}
<li> <li>
<button <button
hx-post="/select_tag/{{ tag.name }}" hx-post="/select_tag/{{ tag.value }}"
hx-target="previous .tags" hx-target="previous .tags"
hx-swap="outerHTML" hx-swap="outerHTML"
> >