Add support for hashtag on user interface

Add migration to fix typo
Add support for linking hashtags with posts
Rework tag search page so it says a nicer message than page not found
when no post use that tag
Add new string to translation
This commit is contained in:
Trinity Pointard 2018-10-20 19:27:49 +02:00
parent 4fa3a0f6ee
commit 95ea248518
16 changed files with 96 additions and 28 deletions

View file

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
ALTER TABLE tags RENAME COLUMN is_hashtag TO is_hastag;

View file

@ -0,0 +1 @@
ALTER TABLE tags RENAME COLUMN is_hastag TO is_hashtag;

View file

@ -0,0 +1,10 @@
CREATE TABLE tags2 (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
tag TEXT NOT NULL DEFAULT '',
is_hastag BOOLEAN NOT NULL DEFAULT 'f',
post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE NOT NULL
);
INSERT INTO tags2 SELECT * FROM tags;
DROP TABLE tags;
ALTER TABLE tags2 RENAME TO tags;

View file

@ -0,0 +1,10 @@
CREATE TABLE tags2 (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
tag TEXT NOT NULL DEFAULT '',
is_hashtag BOOLEAN NOT NULL DEFAULT 'f',
post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE NOT NULL
);
INSERT INTO tags2 SELECT * FROM tags;
DROP TABLE tags;
ALTER TABLE tags2 RENAME TO tags;

View file

@ -68,7 +68,7 @@ pub fn md_to_html(md: &str) -> (String, Vec<String>, Vec<String>) {
} else { } else {
text_acc text_acc
}; };
let link = Tag::Link(format!("/tag/{}", hashtag).into(), hashtag.to_string().into()); let link = Tag::Link(format!("/tag/{}", hashtag.to_camel_case()).into(), hashtag.to_string().into());
hashtags.push(hashtag.clone()); hashtags.push(hashtag.clone());
events.push(Event::Start(link.clone())); events.push(Event::Start(link.clone()));

View file

@ -144,7 +144,7 @@ table! {
tags (id) { tags (id) {
id -> Int4, id -> Int4,
tag -> Text, tag -> Text,
is_hastag -> Bool, is_hashtag -> Bool,
post_id -> Int4, post_id -> Int4,
} }
} }

View file

@ -9,7 +9,7 @@ use schema::tags;
pub struct Tag { pub struct Tag {
pub id: i32, pub id: i32,
pub tag: String, pub tag: String,
pub is_hastag: bool, pub is_hashtag: bool,
pub post_id: i32 pub post_id: i32
} }
@ -17,7 +17,7 @@ pub struct Tag {
#[table_name = "tags"] #[table_name = "tags"]
pub struct NewTag { pub struct NewTag {
pub tag: String, pub tag: String,
pub is_hastag: bool, pub is_hashtag: bool,
pub post_id: i32 pub post_id: i32
} }
@ -40,7 +40,7 @@ impl Tag {
pub fn from_activity(conn: &Connection, tag: Hashtag, post: i32) -> Tag { pub fn from_activity(conn: &Connection, tag: Hashtag, post: i32) -> Tag {
Tag::insert(conn, NewTag { Tag::insert(conn, NewTag {
tag: tag.name_string().expect("Tag::from_activity: name error"), tag: tag.name_string().expect("Tag::from_activity: name error"),
is_hastag: false, is_hashtag: false,
post_id: post post_id: post
}) })
} }

View file

@ -608,3 +608,6 @@ msgstr ""
msgid "This post isn't published yet." msgid "This post isn't published yet."
msgstr "" msgstr ""
msgid "There is currently no article with that tag"
msgstr ""

View file

@ -624,3 +624,6 @@ msgstr "Utilisateurs"
msgid "This post isn't published yet." msgid "This post isn't published yet."
msgstr "Cet article nest pas encore publié." msgstr "Cet article nest pas encore publié."
msgid "There is currently no article with that tag"
msgstr "Il n'y a pas encore d'article avec ce tag"

View file

@ -611,3 +611,6 @@ msgstr "Usuarias"
#, fuzzy #, fuzzy
msgid "This post isn't published yet." msgid "This post isn't published yet."
msgstr "Esto é un borrador, non publicar por agora." msgstr "Esto é un borrador, non publicar por agora."
msgid "There is currently no article with that tag"
msgstr ""

View file

@ -633,6 +633,9 @@ msgstr "Brukernavn"
msgid "This post isn't published yet." msgid "This post isn't published yet."
msgstr "" msgstr ""
msgid "There is currently no article with that tag"
msgstr ""
#~ msgid "One reshare" #~ msgid "One reshare"
#~ msgid_plural "{{ count }} reshares" #~ msgid_plural "{{ count }} reshares"
#~ msgstr[0] "Én deling" #~ msgstr[0] "Én deling"

View file

@ -623,6 +623,9 @@ msgstr "Użytkownicy"
msgid "This post isn't published yet." msgid "This post isn't published yet."
msgstr "Ten wpis nie został jeszcze opublikowany." msgstr "Ten wpis nie został jeszcze opublikowany."
msgid "There is currently no article with that tag"
msgstr ""
#~ msgid "One reshare" #~ msgid "One reshare"
#~ msgid_plural "{{ count }} reshares" #~ msgid_plural "{{ count }} reshares"
#~ msgstr[0] "Jedno udostępnienie" #~ msgstr[0] "Jedno udostępnienie"

View file

@ -591,3 +591,6 @@ msgstr ""
msgid "This post isn't published yet." msgid "This post isn't published yet."
msgstr "" msgstr ""
msgid "There is currently no article with that tag"
msgstr ""

View file

@ -139,7 +139,7 @@ fn edit(blog: String, slug: String, user: User, conn: DbConn) -> Option<Template
content: source, content: source,
tags: Tag::for_post(&*conn, post.id) tags: Tag::for_post(&*conn, post.id)
.into_iter() .into_iter()
.map(|t| t.tag) .filter_map(|t| if !t.is_hashtag {Some(t.tag)} else {None})
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join(", "), .join(", "),
license: post.license.clone(), license: post.license.clone(),
@ -183,7 +183,7 @@ fn update(blog: String, slug: String, user: User, conn: DbConn, data: LenientFor
// actually it's not "Ok"… // actually it's not "Ok"…
Ok(Redirect::to(uri!(super::blogs::details: name = blog))) Ok(Redirect::to(uri!(super::blogs::details: name = blog)))
} else { } else {
let (content, mentions, _hashtag) = utils::md_to_html(form.content.to_string().as_ref());//TODO do something with hashtags let (content, mentions, hashtags) = utils::md_to_html(form.content.to_string().as_ref());
let license = if form.license.len() > 0 { let license = if form.license.len() > 0 {
form.license.to_string() form.license.to_string()
@ -215,16 +215,32 @@ fn update(blog: String, slug: String, user: User, conn: DbConn, data: LenientFor
let old_tags = Tag::for_post(&*conn, post.id).into_iter().collect::<Vec<_>>(); let old_tags = Tag::for_post(&*conn, post.id).into_iter().collect::<Vec<_>>();
let tags = form.tags.split(",").map(|t| t.trim().to_camel_case()).filter(|t| t.len() > 0).collect::<Vec<_>>(); let tags = form.tags.split(",").map(|t| t.trim().to_camel_case()).filter(|t| t.len() > 0).collect::<Vec<_>>();
for tag in tags.iter() { for tag in tags.iter() {
if old_tags.iter().all(|ot| &ot.tag!=tag) { if old_tags.iter().all(|ot| &ot.tag!=tag || ot.is_hashtag) {
Tag::insert(&*conn, NewTag { Tag::insert(&*conn, NewTag {
tag: tag.clone(), tag: tag.clone(),
is_hastag: false, is_hashtag: false,
post_id: post.id post_id: post.id
}); });
} }
} }
for ot in old_tags.iter() {
if !tags.contains(&ot.tag) && !ot.is_hashtag {
ot.delete(&conn);
}
}
let hashtags = hashtags.into_iter().map(|h| h.to_camel_case()).collect::<Vec<_>>();
for hashtag in hashtags.iter() {
if old_tags.iter().all(|ot| &ot.tag!=hashtag || !ot.is_hashtag) {
Tag::insert(&*conn, NewTag {
tag: hashtag.clone(),
is_hashtag: true,
post_id: post.id,
});
}
}
for ot in old_tags { for ot in old_tags {
if !tags.contains(&ot.tag) { if !hashtags.contains(&ot.tag) && ot.is_hashtag {
ot.delete(&conn); ot.delete(&conn);
} }
} }
@ -294,7 +310,7 @@ fn create(blog_name: String, data: LenientForm<NewPostForm>, user: User, conn: D
// actually it's not "Ok"… // actually it's not "Ok"…
Ok(Redirect::to(uri!(super::blogs::details: name = blog_name))) Ok(Redirect::to(uri!(super::blogs::details: name = blog_name)))
} else { } else {
let (content, mentions, _hashtag) = utils::md_to_html(form.content.to_string().as_ref());//TODO do something with hashtags let (content, mentions, hashtags) = utils::md_to_html(form.content.to_string().as_ref());
let post = Post::insert(&*conn, NewPost { let post = Post::insert(&*conn, NewPost {
blog_id: blog.id, blog_id: blog.id,
@ -322,7 +338,14 @@ fn create(blog_name: String, data: LenientForm<NewPostForm>, user: User, conn: D
for tag in tags { for tag in tags {
Tag::insert(&*conn, NewTag { Tag::insert(&*conn, NewTag {
tag: tag, tag: tag,
is_hastag: false, is_hashtag: false,
post_id: post.id
});
}
for hashtag in hashtags {
Tag::insert(&*conn, NewTag {
tag: hashtag.to_camel_case(),
is_hashtag: true,
post_id: post.id post_id: post.id
}); });
} }

View file

@ -4,25 +4,23 @@ use serde_json;
use plume_models::{ use plume_models::{
db_conn::DbConn, db_conn::DbConn,
posts::Post, posts::Post,
tags::Tag,
users::User, users::User,
}; };
use routes::Page; use routes::Page;
#[get("/tag/<name>")] #[get("/tag/<name>")]
fn tag(user: Option<User>, conn: DbConn, name: String) -> Option<Template> { fn tag(user: Option<User>, conn: DbConn, name: String) -> Template {
paginated_tag(user, conn, name, Page::first()) paginated_tag(user, conn, name, Page::first())
} }
#[get("/tag/<name>?<page>")] #[get("/tag/<name>?<page>")]
fn paginated_tag(user: Option<User>, conn: DbConn, name: String, page: Page) -> Option<Template> { fn paginated_tag(user: Option<User>, conn: DbConn, name: String, page: Page) -> Template {
let tag = Tag::find_by_name(&*conn, name)?; let posts = Post::list_by_tag(&*conn, name.clone(), page.limits());
let posts = Post::list_by_tag(&*conn, tag.tag.clone(), page.limits()); Template::render("tags/index", json!({
Some(Template::render("tags/index", json!({ "tag": name.clone(),
"tag": tag.clone(),
"account": user.map(|u| u.to_json(&*conn)), "account": user.map(|u| u.to_json(&*conn)),
"articles": posts.into_iter().map(|p| p.to_json(&*conn)).collect::<Vec<serde_json::Value>>(), "articles": posts.into_iter().map(|p| p.to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
"page": page.page, "page": page.page,
"n_pages": Page::total(Post::count_for_tag(&*conn, tag.tag) as i32) "n_pages": Page::total(Post::count_for_tag(&*conn, name) as i32)
}))) }))
} }

View file

@ -2,16 +2,22 @@
{% import "macros" as macros %} {% import "macros" as macros %}
{% block title %} {% block title %}
{{ 'Articles tagged "{{ tag }}"' | _(tag=tag.tag) }} {{ 'Articles tagged "{{ tag }}"' | _(tag=tag) }}
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
<h1>{{ 'Articles tagged "{{ tag }}"' | _(tag=tag.tag) }}</h1> <h1>{{ 'Articles tagged "{{ tag }}"' | _(tag=tag) }}</h1>
{% if articles| length != 0 %}
<div class="cards"> <div class="cards">
{% for article in articles %} {% for article in articles %}
{{ macros::post_card(article=article) }} {{ macros::post_card(article=article) }}
{% endfor %} {% endfor %}
</div> </div>
{% else %}
<section>
<h2>{{ "There is currently no article with that tag" | _ }}</h2>
</section>
{% endif %}
{{ macros::paginate(page=page, total=n_pages) }} {{ macros::paginate(page=page, total=n_pages) }}
{% endblock content %} {% endblock content %}