Merge pull request 'Add shortcut links to edit page' (#883) from shortcut-links into main

Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/883
This commit is contained in:
KitaitiMakoto 2021-01-24 17:16:11 +00:00
commit 76f7b5e7ac
12 changed files with 505 additions and 313 deletions

View File

@ -21,6 +21,7 @@ executors:
RUST_TEST_THREADS: 1 RUST_TEST_THREADS: 1
FEATURES: <<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>> FEATURES: <<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>>
DATABASE_URL: <<#parameters.postgres>>postgres://postgres@localhost/plume<</parameters.postgres>><<^parameters.postgres>>plume.sqlite<</parameters.postgres>> DATABASE_URL: <<#parameters.postgres>>postgres://postgres@localhost/plume<</parameters.postgres>><<^parameters.postgres>>plume.sqlite<</parameters.postgres>>
ROCKET_SECRET_KEY: VN5xV1DN7XdpATadOCYcuGeR/dV0hHfgx9mx9TarLdM=
commands: commands:
@ -143,12 +144,14 @@ jobs:
cache: <<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>> cache: <<#parameters.postgres>>postgres<</ parameters.postgres>><<^parameters.postgres>>sqlite<</parameters.postgres>>
- run_with_coverage: - run_with_coverage:
cmd: | cmd: |
cargo run -p plume-cli --no-default-features --features=${FEATURES} -- migration run cargo build -p plume-cli --no-default-features --features=${FEATURES} -j1
./target/debug/plm migration run
./target/debug/plm search init
cmd="cargo test --all --exclude plume-front --exclude plume-macro --no-run --no-default-features --features=${FEATURES} -j" cmd="cargo test --all --exclude plume-front --exclude plume-macro --no-run --no-default-features --features=${FEATURES} -j"
for i in 36 4 2 1 1; do for i in 36 4 2 1 1; do
$cmd $i && break $cmd $i && break
done done
cargo test --all --exclude plume-front --exclude plume-macro --no-default-features --features="${FEATURES}" -j1 -- --test-threads=1 cargo test --all --exclude plume-front --exclude plume-macro --no-default-features --features="${FEATURES}" -j1
- upload_coverage: - upload_coverage:
type: unit type: unit
- cache: - cache:

View File

@ -183,6 +183,8 @@ p.error {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: relative;
min-width: 20em; min-width: 20em;
min-height: 20em; min-height: 20em;
margin: 1em; margin: 1em;
@ -225,11 +227,14 @@ p.error {
} }
h3 { h3 {
margin: 0.75em 20px; flex-grow: 1;
margin: 0;
font-family: $playfair; font-family: $playfair;
font-size: 1.75em; font-size: 1.75em;
font-weight: normal; font-weight: normal;
line-height: 1.75;
a { a {
display: block;
transition: color 0.1s ease-in; transition: color 0.1s ease-in;
color: $text-color; color: $text-color;
@ -237,6 +242,15 @@ p.error {
} }
} }
.controls {
float: right;
.button {
margin-top: 0;
margin-bottom: 0;
}
}
main { main {
flex: 1; flex: 1;

View File

@ -247,6 +247,7 @@ pub(crate) mod tests {
use diesel::Connection; use diesel::Connection;
pub(crate) fn fill_database(conn: &Conn) -> Vec<(NewInstance, Instance)> { pub(crate) fn fill_database(conn: &Conn) -> Vec<(NewInstance, Instance)> {
diesel::delete(instances::table).execute(conn).unwrap();
let res = vec![ let res = vec![
NewInstance { NewInstance {
default_license: "WTFPL".to_string(), default_license: "WTFPL".to_string(),

View File

@ -12,6 +12,7 @@ use activitypub::{
use chrono::{NaiveDateTime, TimeZone, Utc}; use chrono::{NaiveDateTime, TimeZone, Utc};
use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl}; use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
use heck::KebabCase; use heck::KebabCase;
use once_cell::sync::Lazy;
use plume_common::{ use plume_common::{
activity_pub::{ activity_pub::{
inbox::{AsObject, FromId}, inbox::{AsObject, FromId},
@ -20,11 +21,13 @@ use plume_common::{
utils::md_to_html, utils::md_to_html,
}; };
use riker::actors::{Publish, Tell}; use riker::actors::{Publish, Tell};
use std::collections::HashSet; use std::collections::{HashMap, HashSet};
use std::sync::Arc; use std::sync::{Arc, Mutex};
pub type LicensedArticle = CustomObject<Licensed, Article>; pub type LicensedArticle = CustomObject<Licensed, Article>;
static BLOG_FQN_CACHE: Lazy<Mutex<HashMap<i32, String>>> = Lazy::new(|| Mutex::new(HashMap::new()));
#[derive(Queryable, Identifiable, Clone, AsChangeset, Debug)] #[derive(Queryable, Identifiable, Clone, AsChangeset, Debug)]
#[changeset_options(treat_none_as_null = "true")] #[changeset_options(treat_none_as_null = "true")]
pub struct Post { pub struct Post {
@ -275,6 +278,24 @@ impl Post {
.map_err(Error::from) .map_err(Error::from)
} }
/// This method exists for use in templates to reduce database access.
/// This should not be used for other purpose.
///
/// This caches query result. The best way to cache query result is holding it in `Post`s field
/// but Diesel doesn't allow it currently.
/// If sometime Diesel allow it, this method should be removed.
pub fn get_blog_fqn(&self, conn: &Connection) -> String {
if let Some(blog_fqn) = BLOG_FQN_CACHE.lock().unwrap().get(&self.blog_id) {
return blog_fqn.to_string();
}
let blog_fqn = self.get_blog(conn).unwrap().fqn;
BLOG_FQN_CACHE
.lock()
.unwrap()
.insert(self.blog_id, blog_fqn.clone());
blog_fqn
}
pub fn count_likes(&self, conn: &Connection) -> Result<i64> { pub fn count_likes(&self, conn: &Connection) -> Result<i64> {
use crate::schema::likes; use crate::schema::likes;
likes::table likes::table

View File

@ -79,7 +79,6 @@ impl ActorFactoryArgs<(Arc<Searcher>, DbPool)> for SearchActor {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::diesel::Connection; use crate::diesel::Connection;
use crate::diesel::RunQueryDsl;
use crate::{ use crate::{
blog_authors::{BlogAuthor, NewBlogAuthor}, blog_authors::{BlogAuthor, NewBlogAuthor},
blogs::{Blog, NewBlog}, blogs::{Blog, NewBlog},
@ -114,7 +113,7 @@ mod tests {
let conn = db_pool.clone().get().unwrap(); let conn = db_pool.clone().get().unwrap();
let title = random_hex()[..8].to_owned(); let title = random_hex()[..8].to_owned();
let (instance, user, blog) = fill_database(&conn); let (_instance, _user, blog) = fill_database(&conn);
let author = &blog.list_authors(&conn).unwrap()[0]; let author = &blog.list_authors(&conn).unwrap()[0];
let post = Post::insert( let post = Post::insert(
@ -151,11 +150,6 @@ mod tests {
searcher.search_document(&conn, Query::from_str(&title).unwrap(), (0, 1))[0].id, searcher.search_document(&conn, Query::from_str(&title).unwrap(), (0, 1))[0].id,
post_id post_id
); );
// TODO: Make sure records are deleted even when assertion failed
post.delete(&conn).unwrap();
blog.delete(&conn).unwrap();
user.delete(&conn).unwrap();
diesel::delete(&instance).execute(&conn).unwrap();
} }
fn fill_database(conn: &Conn) -> (Instance, User, Blog) { fn fill_database(conn: &Conn) -> (Instance, User, Blog) {
@ -164,7 +158,7 @@ mod tests {
conn, conn,
NewInstance { NewInstance {
default_license: "CC-0-BY-SA".to_string(), default_license: "CC-0-BY-SA".to_string(),
local: true, local: false,
long_description: SafeString::new("Good morning"), long_description: SafeString::new("Good morning"),
long_description_html: "<p>Good morning</p>".to_string(), long_description_html: "<p>Good morning</p>".to_string(),
short_description: SafeString::new("Hello"), short_description: SafeString::new("Hello"),
@ -175,14 +169,29 @@ mod tests {
}, },
) )
.unwrap(); .unwrap();
let mut user = NewUser::default(); let user = User::insert(
user.instance_id = instance.id; conn,
user.username = random_hex().to_string(); NewUser {
user.ap_url = random_hex().to_string(); username: random_hex().to_string(),
user.inbox_url = random_hex().to_string(); display_name: random_hex().to_string(),
user.outbox_url = random_hex().to_string(); outbox_url: random_hex().to_string(),
user.followers_endpoint = random_hex().to_string(); inbox_url: random_hex().to_string(),
let user = User::insert(conn, user).unwrap(); summary: "".to_string(),
email: None,
hashed_password: None,
instance_id: instance.id,
ap_url: random_hex().to_string(),
private_key: None,
public_key: "".to_string(),
shared_inbox_url: None,
followers_endpoint: random_hex().to_string(),
avatar_id: None,
summary_html: SafeString::new(""),
role: 0,
fqn: random_hex().to_string(),
},
)
.unwrap();
let mut blog = NewBlog::default(); let mut blog = NewBlog::default();
blog.instance_id = instance.id; blog.instance_id = instance.id;
blog.actor_id = random_hex().to_string(); blog.actor_id = random_hex().to_string();

View File

@ -60,7 +60,7 @@ fn init_pool() -> Option<DbPool> {
Some(pool) Some(pool)
} }
fn main() { pub(crate) fn init_rocket() -> rocket::Rocket {
match dotenv::dotenv() { match dotenv::dotenv() {
Ok(path) => eprintln!("Configuration read from {}", path.display()), Ok(path) => eprintln!("Configuration read from {}", path.display()),
Err(ref e) if e.not_found() => eprintln!("no .env was found"), Err(ref e) if e.not_found() => eprintln!("no .env was found"),
@ -126,7 +126,7 @@ Then try to restart Plume.
warn!("Please refer to the documentation to see how to configure it."); warn!("Please refer to the documentation to see how to configure it.");
} }
let rocket = rocket::custom(CONFIG.rocket.clone().unwrap()) rocket::custom(CONFIG.rocket.clone().unwrap())
.mount( .mount(
"/", "/",
routes![ routes![
@ -268,9 +268,14 @@ Then try to restart Plume.
]) ])
.finalize() .finalize()
.expect("main: csrf fairing creation error"), .expect("main: csrf fairing creation error"),
); )
}
fn main() {
let rocket = init_rocket();
#[cfg(feature = "test")] #[cfg(feature = "test")]
let rocket = rocket.mount("/test", routes![test_routes::health,]); let rocket = rocket.mount("/test", routes![test_routes::health,]);
rocket.launch(); rocket.launch();
} }

View File

@ -371,3 +371,135 @@ pub fn atom_feed(name: String, rockets: PlumeRocket) -> Option<Content<String>>
feed.to_string(), feed.to_string(),
)) ))
} }
#[cfg(test)]
mod tests {
use crate::init_rocket;
use diesel::Connection;
use plume_common::utils::random_hex;
use plume_models::{
blog_authors::{BlogAuthor, NewBlogAuthor},
blogs::{Blog, NewBlog},
db_conn::{DbConn, DbPool},
instance::{Instance, NewInstance},
post_authors::{NewPostAuthor, PostAuthor},
posts::{NewPost, Post},
safe_string::SafeString,
users::{NewUser, User, AUTH_COOKIE},
};
use rocket::{
http::{Cookie, Cookies, SameSite},
local::{Client, LocalRequest},
};
#[test]
fn edit_link_within_post_card() {
let rocket = init_rocket();
let client = Client::new(rocket).expect("valid rocket instance");
let dbpool = client.rocket().state::<DbPool>().unwrap();
let conn = &DbConn(dbpool.get().unwrap());
let (_instance, user, blog, post) = create_models(conn);
let blog_path = uri!(super::activity_details: name = &blog.fqn).to_string();
let edit_link = uri!(
super::super::posts::edit: blog = &blog.fqn,
slug = &post.slug
)
.to_string();
let mut response = client.get(&blog_path).dispatch();
let body = response.body_string().unwrap();
assert!(!body.contains(&edit_link));
let request = client.get(&blog_path);
login(&request, &user);
let mut response = request.dispatch();
let body = response.body_string().unwrap();
assert!(body.contains(&edit_link));
}
fn create_models(conn: &DbConn) -> (Instance, User, Blog, Post) {
conn.transaction::<(Instance, User, Blog, Post), diesel::result::Error, _>(|| {
let instance = Instance::get_local().unwrap_or_else(|_| {
let instance = Instance::insert(
conn,
NewInstance {
default_license: "CC-0-BY-SA".to_string(),
local: true,
long_description: SafeString::new("Good morning"),
long_description_html: "<p>Good morning</p>".to_string(),
short_description: SafeString::new("Hello"),
short_description_html: "<p>Hello</p>".to_string(),
name: random_hex().to_string(),
open_registrations: true,
public_domain: random_hex().to_string(),
},
)
.unwrap();
Instance::cache_local(conn);
instance
});
let mut user = NewUser::default();
user.instance_id = instance.id;
user.username = random_hex().to_string();
user.ap_url = random_hex().to_string();
user.inbox_url = random_hex().to_string();
user.outbox_url = random_hex().to_string();
user.followers_endpoint = random_hex().to_string();
let user = User::insert(conn, user).unwrap();
let mut blog = NewBlog::default();
blog.instance_id = instance.id;
blog.actor_id = random_hex().to_string();
blog.ap_url = random_hex().to_string();
blog.inbox_url = random_hex().to_string();
blog.outbox_url = random_hex().to_string();
let blog = Blog::insert(conn, blog).unwrap();
BlogAuthor::insert(
conn,
NewBlogAuthor {
blog_id: blog.id,
author_id: user.id,
is_owner: true,
},
)
.unwrap();
let post = Post::insert(
conn,
NewPost {
blog_id: blog.id,
slug: random_hex()[..8].to_owned(),
title: random_hex()[..8].to_owned(),
content: SafeString::new(""),
published: true,
license: "CC-By-SA".to_owned(),
ap_url: "".to_owned(),
creation_date: None,
subtitle: "".to_owned(),
source: "".to_owned(),
cover_id: None,
},
)
.unwrap();
PostAuthor::insert(
conn,
NewPostAuthor {
post_id: post.id,
author_id: user.id,
},
)
.unwrap();
Ok((instance, user, blog, post))
})
.unwrap()
}
fn login(request: &LocalRequest, user: &User) {
request.inner().guard::<Cookies>().unwrap().add_private(
Cookie::build(AUTH_COOKIE, user.id.to_string())
.same_site(SameSite::Lax)
.finish(),
);
}
}

View File

@ -8,11 +8,18 @@
@if article.cover_id.is_some() { @if article.cover_id.is_some() {
<div class="cover" style="background-image: url('@Html(article.cover_url(ctx.0).unwrap_or_default())')"></div> <div class="cover" style="background-image: url('@Html(article.cover_url(ctx.0).unwrap_or_default())')"></div>
} }
<header>
@if ctx.2.clone().and_then(|u| article.is_author(ctx.0, u.id).ok()).unwrap_or(false) {
<div class="controls">
<a class="button" href="@uri!(posts::edit: blog = &article.get_blog_fqn(ctx.0), slug = &article.slug)">@i18n!(ctx.1, "Edit")</a>
</div>
}
<h3 class="p-name" dir="auto"> <h3 class="p-name" dir="auto">
<a class="u-url" href="@uri!(posts::details: blog = article.get_blog(ctx.0).unwrap().fqn, slug = &article.slug, responding_to = _)"> <a class="u-url" href="@uri!(posts::details: blog = article.get_blog_fqn(ctx.0), slug = &article.slug, responding_to = _)">
@article.title @article.title
</a> </a>
</h3> </h3>
</header>
<main> <main>
<p class="p-summary" dir="auto">@article.subtitle</p> <p class="p-summary" dir="auto">@article.subtitle</p>
</main> </main>
@ -26,7 +33,7 @@
@if article.published { @if article.published {
<span class="dt-published" datetime="@article.creation_date.format("%F %T")">@article.creation_date.format("%B %e, %Y")</span> <span class="dt-published" datetime="@article.creation_date.format("%F %T")">@article.creation_date.format("%B %e, %Y")</span>
} }
<a href="@uri!(blogs::details: name = &article.get_blog(ctx.0).unwrap().fqn, page = _)">@article.get_blog(ctx.0).unwrap().title</a> <a href="@uri!(blogs::details: name = &article.get_blog_fqn(ctx.0), page = _)">@article.get_blog(ctx.0).unwrap().title</a>
</div> </div>
@if !article.published { @if !article.published {