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

@ -1,3 +1,3 @@
* { * {
font-family: monospace; font-family: monospace;
} }

View File

@ -64,37 +64,37 @@ main header.article {
} }
main .article-info { main .article-info {
margin: 0 auto 3em; margin: 0 auto 3em;
font-size: 0.95em; font-size: 0.95em;
font-weight: 400; font-weight: 400;
.author, .author a { .author, .author a {
font-weight: 600; font-weight: 600;
} }
} }
/* The article itself */ /* The article itself */
main article { main article {
max-width: $article-width; max-width: $article-width;
margin: 2.5em auto; margin: 2.5em auto;
font-family: $lora; font-family: $lora;
font-size: 1.2em; font-size: 1.2em;
line-height: 1.7; line-height: 1.7;
a:hover { a:hover {
text-decoration: underline; text-decoration: underline;
} }
img { img {
display: block; display: block;
margin: 3em auto; margin: 3em auto;
max-width: 100%; max-width: 100%;
} }
pre { pre {
padding: 1em; padding: 1em;
background: $gray; background: $gray;
overflow: auto; overflow: auto;
} }
blockquote { blockquote {
@ -126,7 +126,7 @@ main .article-meta {
> p { > p {
margin: 2em $horizontal-margin; margin: 2em $horizontal-margin;
font-size: 0.9em; font-size: 0.9em;
} }
/* Article Tags */ /* Article Tags */
@ -157,15 +157,15 @@ main .article-meta {
/* Likes & Boosts */ /* Likes & Boosts */
.actions { .actions {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-around; justify-content: space-around;
} }
.likes, .reshares { .likes, .reshares {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
padding: 0.5em 0; padding: 0.5em 0;
p { p {
font-size: 1.5em; font-size: 1.5em;
@ -175,34 +175,34 @@ main .article-meta {
.action { .action {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin: 0; margin: 0;
padding: 0; padding: 0;
background: none; background: none;
color: $text-color; color: $text-color;
border: none; border: none;
font-size: 1.1em; font-size: 1.1em;
cursor: pointer; cursor: pointer;
svg.feather { svg.feather {
transition: background 0.1s ease-in; transition: background 0.1s ease-in;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin: 0.5em 0; margin: 0.5em 0;
width: 2.5em; width: 2.5em;
height: 2.5em; height: 2.5em;
border-radius: 50%; border-radius: 50%;
} }
&.reshared, &.liked { &.reshared, &.liked {
svg.feather { svg.feather {
color: $background; color: $background;
font-weight: 900; font-weight: 900;
} }
} }
} }
@ -213,14 +213,14 @@ main .article-meta {
.action svg.feather { .action svg.feather {
padding: 0.7em; padding: 0.7em;
box-sizing: border-box; box-sizing: border-box;
color: $red; color: $red;
fill: none; fill: none;
border: solid $red thin; border: solid $red thin;
} }
.action:hover svg.feather { .action:hover svg.feather {
background: transparentize($red, 0.85); background: transparentize($red, 0.85);
} }
.action.liked svg.feather { .action.liked svg.feather {
@ -238,22 +238,22 @@ main .article-meta {
.action svg.feather { .action svg.feather {
padding: 0.7em; padding: 0.7em;
box-sizing: border-box; box-sizing: border-box;
color: $primary; color: $primary;
border: solid $primary thin; border: solid $primary thin;
font-weight: 600; font-weight: 600;
} }
.action:hover svg.feather { .action:hover svg.feather {
background: transparentize($primary, 0.85); background: transparentize($primary, 0.85);
} }
.action.reshared svg.feather { .action.reshared svg.feather {
background: $primary; background: $primary;
} }
.action.reshared:hover svg.feather { .action.reshared:hover svg.feather {
background: transparentize($primary, 0.75) background: transparentize($primary, 0.75)
color: $primary; color: $primary;
} }
} }
@ -262,9 +262,9 @@ main .article-meta {
margin: 0 $horizontal-margin; margin: 0 $horizontal-margin;
h2 { h2 {
color: $primary; color: $primary;
font-size: 1.5em; font-size: 1.5em;
font-weight: 600; font-weight: 600;
} }
summary { summary {
@ -279,16 +279,16 @@ main .article-meta {
// Respond & delete comment buttons // Respond & delete comment buttons
a.button, form.inline, form.inline input { a.button, form.inline, form.inline input {
padding: 0; padding: 0;
background: none; background: none;
color: $text-color; color: $text-color;
margin-right: 2em; margin-right: 2em;
font-family: $route159; font-family: $route159;
font-weight: normal; font-weight: normal;
&::before { &::before {
color: $primary; color: $primary;
padding-right: 0.5em; padding-right: 0.5em;
} }
&:hover { color: $primary; } &:hover { color: $primary; }
@ -296,8 +296,8 @@ main .article-meta {
.comment { .comment {
margin: 1em 0; margin: 1em 0;
font-size: 1em; font-size: 1em;
border: none; border: none;
.content { .content {
background: $gray; background: $gray;
@ -328,36 +328,36 @@ main .article-meta {
color: transparentize($text-color, 0.6); color: transparentize($text-color, 0.6);
} }
.author { .author {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
align-content: center; align-content: center;
* { * {
transition: all 0.1s ease-in; transition: all 0.1s ease-in;
} }
.display-name { .display-name {
color: $text-color; color: $text-color;
} }
&:hover { &:hover {
.display-name { color: $primary; } .display-name { color: $primary; }
small { opacity: 1; } small { opacity: 1; }
} }
} }
& > .comment { & > .comment {
padding-left: 2em; padding-left: 2em;
} }
.text { .text {
padding: 1.25em 0; padding: 1.25em 0;
font-family: $lora; font-family: $lora;
font-size: 1.1em; font-size: 1.1em;
line-height: 1.4; line-height: 1.4;
text-align: left; text-align: left;
} }
} }
} }

View File

@ -1,27 +1,27 @@
label { label {
display: block; display: block;
margin: 2em auto .5em; margin: 2em auto .5em;
font-size: 1.2em; font-size: 1.2em;
} }
input, textarea, select { input, textarea, select {
transition: all 0.1s ease-in; transition: all 0.1s ease-in;
display: block; display: block;
width: 100%; width: 100%;
margin: auto; margin: auto;
padding: 1em; padding: 1em;
box-sizing: border-box; box-sizing: border-box;
-webkit-appearance: textarea; -webkit-appearance: textarea;
background: $form-input-background; background: $form-input-background;
color: $text-color; color: $text-color;
border: solid $form-input-border thin; border: solid $form-input-border thin;
font-size: 1.2em; font-size: 1.2em;
font-weight: 400; font-weight: 400;
&:focus { &:focus {
border-color: $primary; border-color: $primary;
} }
} }
form input[type="submit"] { form input[type="submit"] {
margin: 2em auto; margin: 2em auto;
@ -29,18 +29,18 @@ form input[type="submit"] {
} }
textarea { textarea {
resize: vertical; resize: vertical;
overflow-y: scroll; overflow-y: scroll;
font-family: $lora; font-family: $lora;
font-size: 1.1em; font-size: 1.1em;
line-height: 1.5; line-height: 1.5;
} }
input[type="checkbox"] { input[type="checkbox"] {
display: inline; display: inline;
margin: initial; margin: initial;
min-width: initial; min-width: initial;
width: initial; width: initial;
-webkit-appearance: checkbox; -webkit-appearance: checkbox;
} }
@ -71,31 +71,31 @@ form.inline {
} }
.button, .button:visited, input[type="submit"], input[type="submit"].button { .button, .button:visited, input[type="submit"], input[type="submit"].button {
transition: all 0.1s ease-in; transition: all 0.1s ease-in;
display: inline-block; display: inline-block;
-webkit-appearance: none; -webkit-appearance: none;
margin: 0.5em auto; margin: 0.5em auto;
padding: 0.75em 1em; padding: 0.75em 1em;
background: $primary; background: $primary;
color: $primary-text-color; color: $primary-text-color;
font-weight: bold; font-weight: bold;
border: none; border: none;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background: transparentize($primary, 0.1); background: transparentize($primary, 0.1);
} }
&.destructive { &.destructive {
background: $red; background: $red;
&:hover { &:hover {
background: transparentize($red, 0.1); background: transparentize($red, 0.1);
} }
} }
&.secondary { &.secondary {
background: $gray; background: $gray;
@ -115,20 +115,20 @@ input[type="submit"] {
form.new-post { form.new-post {
max-width: 60em; max-width: 60em;
.title { .title {
margin: 0 auto; margin: 0 auto;
padding: 0.75em 0; padding: 0.75em 0;
background: none; background: none;
border: none; border: none;
font-family: $playfair; font-family: $playfair;
font-size: 2em; font-size: 2em;
text-align: left; text-align: left;
} }
textarea { textarea {
min-height: 20em; min-height: 20em;
overflow-y: scroll; overflow-y: scroll;
resize: none; resize: none;
-webkit-appearance: textarea; -webkit-appearance: textarea;
} }
} }

View File

@ -6,43 +6,43 @@ html {
} }
html, body { html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;
background: $background; background: $background;
color: $text-color; color: $text-color;
font-family: $route159; font-family: $route159;
::selection { ::selection {
background: transparentize($primary, 0.7); background: transparentize($primary, 0.7);
} }
::-moz-selection { ::-moz-selection {
background: transparentize($primary, 0.7); background: transparentize($primary, 0.7);
} }
} }
a, a:visited { a, a:visited {
color: $primary; color: $primary;
text-decoration: none; text-decoration: none;
} }
a::selection { a::selection {
color: $background; color: $background;
} }
a::-moz-selection { a::-moz-selection {
color: $background; color: $background;
} }
small { small {
margin-left: 1em; margin-left: 1em;
color: transparentize($text-color, 0.6); color: transparentize($text-color, 0.6);
font-size: 0.75em; font-size: 0.75em;
word-wrap: break-word; word-wrap: break-word;
word-break: break-all; word-break: break-all;
} }
.center { .center {
text-align: center; text-align: center;
font-weight: bold; font-weight: bold;
opacity: 0.6; opacity: 0.6;
padding: 5em; padding: 5em;
} }
.right { .right {
@ -53,28 +53,28 @@ small {
} }
.spaced { .spaced {
margin: 4rem 0; margin: 4rem 0;
} }
.banner { .banner {
background: $gray; background: $gray;
padding-top: 2em; padding-top: 2em;
padding-bottom: 1em; padding-bottom: 1em;
margin: 3em 0px; margin: 3em 0px;
} }
.hidden { .hidden {
display: none; display: none;
appearance: none; appearance: none;
} }
/* Main */ /* Main */
body > main > *, .h-feed > * { body > main > *, .h-feed > * {
margin: 1em $horizontal-margin; margin: 1em $horizontal-margin;
} }
body > main > .h-entry, .h-feed { body > main > .h-entry, .h-feed {
margin: 0; margin: 0;
} }
body > main { body > main {
@ -98,18 +98,18 @@ main {
margin-top: 1em; margin-top: 1em;
&.article { &.article {
margin: 1em auto 0.5em; margin: 1em auto 0.5em;
font-family: $playfair; font-family: $playfair;
font-size: 2.5em; font-size: 2.5em;
font-weight: normal; font-weight: normal;
} }
} }
h2 { h2 {
font-size: 1.75em; font-size: 1.75em;
font-weight: 300; font-weight: 300;
&.article { &.article {
font-size: 1.25em; font-size: 1.25em;
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
@ -139,15 +139,15 @@ main {
/* Errors */ /* Errors */
p.error { p.error {
color: $red; color: $red;
font-weight: bold; font-weight: bold;
} }
/* User page */ /* User page */
.user h1 { .user h1 {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
margin: 0px; margin: 0px;
} }
@ -156,14 +156,14 @@ p.error {
} }
.badge { .badge {
margin-right: 1em; margin-right: 1em;
padding: 0.35em 1em; padding: 0.35em 1em;
background: $background; background: $background;
color: $primary; color: $primary;
border: 1px solid $primary; border: 1px solid $primary;
font-size: 1rem; font-size: 1rem;
} }
.user-summary { .user-summary {
@ -172,23 +172,25 @@ p.error {
/* Cards */ /* Cards */
.cards { .cards {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
padding: 0 5%; padding: 0 5%;
margin: 1rem 0 5rem; margin: 1rem 0 5rem;
} }
.card { .card {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 20em; position: relative;
min-height: 20em;
margin: 1em;
box-sizing: border-box;
background: $gray; min-width: 20em;
min-height: 20em;
margin: 1em;
box-sizing: border-box;
background: $gray;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -213,11 +215,11 @@ p.error {
} }
> * { > * {
margin: 20px; margin: 20px;
} }
.cover { .cover {
min-height: 10em; min-height: 10em;
background-position: center; background-position: center;
background-size: cover; background-size: cover;
@ -225,26 +227,38 @@ p.error {
} }
h3 { h3 {
margin: 0.75em 20px; flex-grow: 1;
font-family: $playfair; margin: 0;
font-size: 1.75em; font-family: $playfair;
font-weight: normal; font-size: 1.75em;
a { font-weight: normal;
transition: color 0.1s ease-in; line-height: 1.75;
color: $text-color; a {
display: block;
transition: color 0.1s ease-in;
color: $text-color;
&:hover { color: $primary; } &:hover { color: $primary; }
} }
}
.controls {
float: right;
.button {
margin-top: 0;
margin-bottom: 0;
}
} }
main { main {
flex: 1; flex: 1;
font-family: $lora; font-family: $lora;
font-size: 1em; font-size: 1em;
line-height: 1.25; line-height: 1.25;
text-align: left; text-align: left;
overflow: hidden; overflow: hidden;
} }
} }
@ -286,15 +300,15 @@ p.error {
/* Stats */ /* Stats */
.stats { .stats {
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
margin: 2em; margin: 2em;
> div { > div {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
p { p {

View File

@ -3,8 +3,8 @@ body > header {
#content { #content {
display: flex; display: flex;
align-content: center; align-content: center;
justify-content: space-between; justify-content: space-between;
} }
nav#menu { nav#menu {
@ -19,44 +19,44 @@ body > header {
a { a {
transform: skewX(15deg); transform: skewX(15deg);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 1.4em; width: 1.4em;
height: 1.4em; height: 1.4em;
margin: 0; margin: 0;
padding: 0; padding: 0;
color: $gray; color: $gray;
font-size: 1.33em; font-size: 1.33em;
} }
} }
nav { nav {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
hr { hr {
height: 100%; height: 100%;
width: 0.2em; width: 0.2em;
background: $primary; background: $primary;
border: none; border: none;
transform: skewX(-15deg); transform: skewX(-15deg);
} }
a { a {
display: flex; display: flex;
align-items: center; align-items: center;
position: relative; position: relative;
align-self: stretch; align-self: stretch;
margin: 0; margin: 0;
padding: 0 2em; padding: 0 2em;
font-size: 1em; font-size: 1em;
i { font-size: 1.2em; } i { font-size: 1.2em; }
&.title { &.title {
margin: 0; margin: 0;
text-align: center; text-align: center;
padding: 0.5em 1em; padding: 0.5em 1em;
font-size: 1.75em; font-size: 1.75em;
@ -70,7 +70,7 @@ body > header {
margin: 0; margin: 0;
padding-left: 0.5em; padding-left: 0.5em;
} }
} }
} }
} }
} }
@ -205,36 +205,36 @@ body > header {
/* Only enable label animations on large screens */ /* Only enable label animations on large screens */
@media screen and (min-width: 600px) { @media screen and (min-width: 600px) {
header nav a { header nav a {
i { i {
transition: all 0.2s ease; transition: all 0.2s ease;
margin: 0; margin: 0;
}
.mobile-label {
transition: all 0.2s ease;
display: block;
position: absolute;
left: 50%;
transform: translate(-50%, 0);
transform: translateZ(0);
-webkit-transform: none !important;
opacity: 0;
font-size: 0.9em;
white-space: nowrap;
}
img + .mobile-label { display: none; }
&:hover {
i { margin-bottom: 0.75em; }
.mobile-label {
opacity: 1;
transform: translate(-50%, 80%);
-webkit-transform: translate(-50%, 80%);
}
} }
}
.mobile-label {
transition: all 0.2s ease;
display: block;
position: absolute;
left: 50%;
transform: translate(-50%, 0);
transform: translateZ(0);
-webkit-transform: none !important;
opacity: 0;
font-size: 0.9em;
white-space: nowrap;
}
img + .mobile-label { display: none; }
&:hover {
i { margin-bottom: 0.75em; }
.mobile-label {
opacity: 1;
transform: translate(-50%, 80%);
-webkit-transform: translate(-50%, 80%);
}
}
}
} }
// Small screens // Small screens

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>
} }
<h3 class="p-name" dir="auto"> <header>
<a class="u-url" href="@uri!(posts::details: blog = article.get_blog(ctx.0).unwrap().fqn, slug = &article.slug, responding_to = _)"> @if ctx.2.clone().and_then(|u| article.is_author(ctx.0, u.id).ok()).unwrap_or(false) {
@article.title <div class="controls">
</a> <a class="button" href="@uri!(posts::edit: blog = &article.get_blog_fqn(ctx.0), slug = &article.slug)">@i18n!(ctx.1, "Edit")</a>
</h3> </div>
}
<h3 class="p-name" dir="auto">
<a class="u-url" href="@uri!(posts::details: blog = article.get_blog_fqn(ctx.0), slug = &article.slug, responding_to = _)">
@article.title
</a>
</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 {