From: David ‘Bombe’ Roden Date: Thu, 2 Jan 2020 21:00:17 +0000 (+0100) Subject: 🔀 Merge “next” into “feature/notification-handlers” X-Git-Tag: v81^2~5^2~24 X-Git-Url: https://git.pterodactylus.net/?p=Sone.git;a=commitdiff_plain;h=146335994dc415527bc4c8ec9a0981d739444ea8;hp=4cdc52febe9b6f3a499da08805a1b03bcc1be852 🔀 Merge “next” into “feature/notification-handlers” --- diff --git a/src/main/java/net/pterodactylus/sone/template/PostAccessor.java b/src/main/java/net/pterodactylus/sone/template/PostAccessor.java deleted file mode 100644 index 4025c7b..0000000 --- a/src/main/java/net/pterodactylus/sone/template/PostAccessor.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Sone - PostAccessor.java - Copyright © 2010–2019 David Roden - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package net.pterodactylus.sone.template; - -import net.pterodactylus.sone.core.Core; -import net.pterodactylus.sone.data.Post; -import net.pterodactylus.sone.data.Reply; -import net.pterodactylus.sone.data.Sone; -import net.pterodactylus.util.template.ReflectionAccessor; -import net.pterodactylus.util.template.TemplateContext; - -import com.google.common.collect.Collections2; - -/** - * Accessor for {@link Post} objects that adds additional properties: - *
- *
replies
- *
All replies to this post, sorted by time, oldest first
- *
- */ -public class PostAccessor extends ReflectionAccessor { - - /** The core to get the replies from. */ - private final Core core; - - /** - * Creates a new post accessor. - * - * @param core - * The core to get the replies from - */ - public PostAccessor(Core core) { - this.core = core; - } - - /** - * {@inheritDoc} - */ - @Override - public Object get(TemplateContext templateContext, Object object, String member) { - Post post = (Post) object; - if ("replies".equals(member)) { - return Collections2.filter(core.getReplies(post.getId()), Reply.FUTURE_REPLY_FILTER); - } else if (member.equals("likes")) { - return core.getLikes(post); - } else if (member.equals("liked")) { - Sone currentSone = (Sone) templateContext.get("currentSone"); - return (currentSone != null) && (currentSone.isLikedPostId(post.getId())); - } else if (member.equals("new")) { - return !post.isKnown(); - } else if (member.equals("bookmarked")) { - return core.isBookmarked(post); - } - return super.get(templateContext, object, member); - } - -} diff --git a/src/main/kotlin/net/pterodactylus/sone/template/PostAccessor.kt b/src/main/kotlin/net/pterodactylus/sone/template/PostAccessor.kt new file mode 100644 index 0000000..5222b94 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/template/PostAccessor.kt @@ -0,0 +1,54 @@ +/* + * Sone - PostAccessor.java - Copyright © 2010–2019 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.pterodactylus.sone.template + +import net.pterodactylus.sone.core.* +import net.pterodactylus.sone.data.* +import net.pterodactylus.util.template.* + +/** + * Accessor for [Post] objects that adds additional properties: + * + * * `replies`: All replies to this post, sorted by time, oldest first + * * `likes`: All Sones that have liked the post + * * `liked`: `true` if the current Sone from the [template context][TemplateContext] has liked the post + * * `new`: `true` if the post is not known + * * `bookmarked`: `true` if the post is bookmarked + */ +class PostAccessor(private val core: Core) : ReflectionAccessor() { + + override fun get(templateContext: TemplateContext?, `object`: Any?, member: String): Any? = + (`object` as Post).let { post -> + when (member) { + "replies" -> core.getReplies(post) + "likes" -> core.getLikes(post) + "liked" -> templateContext.currentSone?.isLikedPostId(post.id) ?: false + "new" -> !post.isKnown + "bookmarked" -> core.isBookmarked(post) + "replySone" -> core.getReplies(post) + .lastOrNull { it.sone.isLocal } + ?.sone + ?: post.sone.takeIf { it.isLocal } + ?: templateContext.currentSone + else -> super.get(templateContext, `object`, member) + } + } + +} + +private fun Core.getReplies(post: Post) = getReplies(post.id).filter { Reply.FUTURE_REPLY_FILTER.apply(it) } +private val TemplateContext?.currentSone: Sone? get() = this?.get("currentSone") as? Sone diff --git a/src/main/resources/i18n/sone.fr.properties b/src/main/resources/i18n/sone.fr.properties index b426bbc..24858aa 100644 --- a/src/main/resources/i18n/sone.fr.properties +++ b/src/main/resources/i18n/sone.fr.properties @@ -52,14 +52,14 @@ Page.Options.Option.ShowAvatars.Followed.Description=Ne montrer que les avatars Page.Options.Option.ShowAvatars.ManuallyTrusted.Description=Ne montrer que les avatars des Sones auxquelles vous êtes assignés manuellement une confiance superieure à 0. Page.Options.Option.ShowAvatars.Trusted.Description=Ne montrer que les avatars des Sones qui ont une confiance superieure à 0. Page.Options.Option.ShowAvatars.Always.Description=Montre tout le temps les avatars personnalisés. Attention: certains avatars peuvent être offensants ! -Page.Options.Section.LoadLinkedImagesOptions.Title=Load Linked Images -Page.Options.Option.LoadLinkedImages.Description=Sone can automatically try to load images that are linked to in posts and replies. This will only ever load images from Freenet, never from the internet! -Page.Options.Option.LoadLinkedImages.Never.Description=Never load linked images. -Page.Options.Option.LoadLinkedImages.Followed.Description=Only load images linked from Sones that you follow. -Page.Options.Option.LoadLinkedImages.ManuallyTrusted.Description=Only load images linked from Sones that you have manually assigned a trust value larger than 0 to. -Page.Options.Option.LoadLinkedImages.Trusted.Description=Only load images linked from Sones that have a trust value larger than 0. -Page.Options.Option.LoadLinkedImages.Always.Description=Always load linked images. Be warned: some images might be disturbing or considered offensive. -Page.Options.Section.RuntimeOptions.Title=Comportement runtime +Page.Options.Section.LoadLinkedImagesOptions.Title=Chargement des images liées +Page.Options.Option.LoadLinkedImages.Description=Sone essaie automatiquement de charger les images liées à des messages ou à des réponses. Tous les chargements sont faits depuis Freenet, jamais depuis internet ! +Page.Options.Option.LoadLinkedImages.Never.Description=Ne jamais charger les images liées. +Page.Options.Option.LoadLinkedImages.Followed.Description=Charge uniquement les images liées à Sones que vous suivez. +Page.Options.Option.LoadLinkedImages.ManuallyTrusted.Description=Ne charger que des images liées des Sones auxquelles vous êtes assignés manuellement une confiance superieure à 0. +Page.Options.Option.LoadLinkedImages.Trusted.Description=Ne charger que des images liées à des Sones qui ont une confiance superieure à 0. +Page.Options.Option.LoadLinkedImages.Always.Description=Charge tout le temps les images liées. Attention: certains avatars peuvent être offensants ! +Page.Options.Section.RuntimeOptions.Title=Comportement de l'exécution Page.Options.Option.InsertionDelay.Description=Le délai d'insertion d'un Sone. Page.Options.Option.PostsPerPage.Description=Le nombre de message à afficher par page avant que les boutons de pagination soit affichés. Page.Options.Option.ImagesPerPage.Description=Le nombre de message à afficher par page avant que les boutons de pagination soit affichés. @@ -192,7 +192,7 @@ Page.ViewSone.Profile.Title=Profile Page.ViewSone.Profile.Label.Name=Nom Page.ViewSone.Profile.Label.Albums=Albums Page.ViewSone.Profile.Albums.Text.All=Tous les albums -Page.ViewSone.Profile.Name.WoTLink=Profile Web of trust +Page.ViewSone.Profile.Name.WoTLink=Profil Web of Trust Page.ViewSone.Replies.Title=Messages {sone} a répondu à Page.ViewPost.Title=Voir message - Sone @@ -321,11 +321,11 @@ Page.Invalid.Title=Action invalide réalisée - Sone Page.Invalid.Page.Title=Action invalide réalisée Page.Invalid.Text=Une action invalide a été effectuée, ou l'action était valide mes les paramètres ne l'étaient pas. Veuillez retourner à la {link}page d'index{/link} et veuillez réessayer. Si l'erreur persiste, vous avez probablement trouvé un bug. -Page.Metrics.Title=Metrics -Page.Metrics.Page.Title=Metrics -Page.Metrics.SoneInsertDuration.Title=Sone Insert Duration -Page.Metrics.SoneParseDuration.Title=Sone Parse Duration -Page.Metrics.ConfigurationSaveDuration.Title=Configuration Save Duration +Page.Metrics.Title=Métriques +Page.Metrics.Page.Title=Métriques +Page.Metrics.SoneInsertDuration.Title=Durée d'insertion de Sone +Page.Metrics.SoneParseDuration.Title=Durée d'analyse de Sone +Page.Metrics.ConfigurationSaveDuration.Title=Durée de sauvegarde de configuration View.Search.Button.Search=Recherche @@ -357,7 +357,7 @@ View.SoneMenu.Link.AllAlbums=Tous les Albums View.SoneMenu.WebOfTrustLink=Profile web of trust View.Post.UnknownAuthor=(inconnu) -View.Post.WebOfTrustLink=Profile web of trust +View.Post.WebOfTrustLink=Profil Web of Trust View.Post.Permalink=Lier le message View.Post.PermalinkAuthor=Lier l'auteur View.Post.Bookmarks.PostIsBookmarked=Ce message a été ajouté aux marque-pages, cliquer pour retirer des marque-pages @@ -461,5 +461,5 @@ Notification.Mention.Text=Vous avez été mentionné dans les messages suivants: Notification.SoneIsInserting.Text=Votre Sone sone://{0} va maintenant être inséré. Notification.SoneIsInserted.Text=votre Sone sone://{0} a été inséré dans {1,number} {1,choice,0#seconds|1#second|1 diff --git a/src/test/java/net/pterodactylus/sone/template/PostAccessorTest.java b/src/test/java/net/pterodactylus/sone/template/PostAccessorTest.java deleted file mode 100644 index e60dce9..0000000 --- a/src/test/java/net/pterodactylus/sone/template/PostAccessorTest.java +++ /dev/null @@ -1,118 +0,0 @@ -package net.pterodactylus.sone.template; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.is; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Set; - -import net.pterodactylus.sone.core.Core; -import net.pterodactylus.sone.data.Post; -import net.pterodactylus.sone.data.PostReply; -import net.pterodactylus.sone.data.Sone; -import net.pterodactylus.util.template.TemplateContext; - -import org.junit.Before; -import org.junit.Test; - -/** - * Unit test for {@link PostAccessor}. - */ -public class PostAccessorTest { - - private final Core core = mock(Core.class); - private final PostAccessor accessor = new PostAccessor(core); - private final Post post = mock(Post.class); - - private final long now = System.currentTimeMillis(); - - @Before - public void setupPost() { - when(post.getId()).thenReturn("post-id"); - } - - @Test - @SuppressWarnings("unchecked") - public void accessorReturnsTheCorrectReplies() { - List replies = new ArrayList<>(); - replies.add(createPostReply(2000)); - replies.add(createPostReply(-1000)); - replies.add(createPostReply(-2000)); - replies.add(createPostReply(-3000)); - replies.add(createPostReply(-4000)); - when(core.getReplies("post-id")).thenReturn(replies); - Collection repliesForPost = (Collection) accessor.get(null, post, "replies"); - assertThat(repliesForPost, contains( - replies.get(1), - replies.get(2), - replies.get(3), - replies.get(4) - )); - } - - private PostReply createPostReply(long timeOffset) { - PostReply postReply = mock(PostReply.class); - when(postReply.getTime()).thenReturn(now + timeOffset); - return postReply; - } - - @Test - @SuppressWarnings("unchecked") - public void accessorReturnsTheLikingSones() { - Set sones = mock(Set.class); - when(core.getLikes(post)).thenReturn(sones); - Set likingSones = (Set) accessor.get(null, post, "likes"); - assertThat(likingSones, is(sones)); - } - - @Test - public void accessorReturnsWhetherTheCurrentSoneLikedAPost() { - Sone sone = mock(Sone.class); - when(sone.isLikedPostId("post-id")).thenReturn(true); - TemplateContext templateContext = new TemplateContext(); - templateContext.set("currentSone", sone); - assertThat(accessor.get(templateContext, post, "liked"), is((Object) true)); - } - - @Test - public void accessorReturnsFalseIfPostIsNotLiked() { - Sone sone = mock(Sone.class); - TemplateContext templateContext = new TemplateContext(); - templateContext.set("currentSone", sone); - assertThat(accessor.get(templateContext, post, "liked"), is((Object) false)); - } - - @Test - public void accessorReturnsFalseIfThereIsNoCurrentSone() { - TemplateContext templateContext = new TemplateContext(); - assertThat(accessor.get(templateContext, post, "liked"), is((Object) false)); - } - - @Test - public void accessorReturnsThatNotKnownPostIsNew() { - assertThat(accessor.get(null, post, "new"), is((Object) true)); - } - - @Test - public void accessorReturnsThatKnownPostIsNotNew() { - when(post.isKnown()).thenReturn(true); - assertThat(accessor.get(null, post, "new"), is((Object) false)); - } - - @Test - public void accessorReturnsIfPostIsBookmarked() { - when(core.isBookmarked(post)).thenReturn(true); - assertThat(accessor.get(null, post, "bookmarked"), is((Object) true)); - } - - @Test - public void accessorReturnsOtherProperties() { - assertThat(accessor.get(null, post, "hashCode"), is((Object) post.hashCode())); - } - -} diff --git a/src/test/kotlin/net/pterodactylus/sone/template/PostAccessorTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/PostAccessorTest.kt new file mode 100644 index 0000000..72e1343 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/template/PostAccessorTest.kt @@ -0,0 +1,187 @@ +package net.pterodactylus.sone.template + +import net.pterodactylus.sone.core.* +import net.pterodactylus.sone.data.* +import net.pterodactylus.sone.test.* +import net.pterodactylus.util.template.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import org.junit.* + +/** + * Unit test for [PostAccessor]. + */ +class PostAccessorTest { + + private val core = mock() + private val accessor = PostAccessor(core) + private val post = mock() + private val now = System.currentTimeMillis() + + @Before + fun setupPost() { + whenever(post.id).thenReturn("post-id") + } + + @Test + fun `accessor returns the correct replies`() { + val replies = listOf( + createPostReply(2000), + createPostReply(-1000), + createPostReply(-2000), + createPostReply(-3000), + createPostReply(-4000) + ) + whenever(core.getReplies("post-id")).thenReturn(replies) + val repliesForPost = accessor[null, post, "replies"] as Collection + assertThat(repliesForPost, contains( + replies[1], + replies[2], + replies[3], + replies[4] + )) + } + + private fun createPostReply(timeOffset: Long) = mock().apply { + whenever(time).thenReturn(now + timeOffset) + } + + @Test + fun `accessor returns the liking sones`() { + val sones = setOf() + whenever(core.getLikes(post)).thenReturn(sones) + val likingSones = accessor[null, post, "likes"] as Set + assertThat(likingSones, equalTo(sones)) + } + + @Test + fun `accessor returns whether the current sone liked a post`() { + val sone = mock() + whenever(sone.isLikedPostId("post-id")).thenReturn(true) + val templateContext = TemplateContext() + templateContext["currentSone"] = sone + assertThat(accessor[templateContext, post, "liked"], equalTo(true)) + } + + @Test + fun `accessor returns false if post is not liked`() { + val sone = mock() + val templateContext = TemplateContext() + templateContext["currentSone"] = sone + assertThat(accessor[templateContext, post, "liked"], equalTo(false)) + } + + @Test + fun `accessor returns false if there is no current sone`() { + val templateContext = TemplateContext() + assertThat(accessor[templateContext, post, "liked"], equalTo(false)) + } + + @Test + fun `accessor returns that not known post is new`() { + assertThat(accessor[null, post, "new"], equalTo(true)) + } + + @Test + fun `accessor returns that known post is not new`() { + whenever(post.isKnown).thenReturn(true) + assertThat(accessor[null, post, "new"], equalTo(false)) + } + + @Test + fun `accessor returns if post is bookmarked`() { + whenever(core.isBookmarked(post)).thenReturn(true) + assertThat(accessor[null, post, "bookmarked"], equalTo(true)) + } + + @Test + fun `reply sone for remote post without replies is current sone`() { + val post = mockPostFrom(remoteSone) + assertThat(accessor[templateContext, post, "replySone"], equalTo(currentSone)) + } + + @Test + fun `reply sone for remote post with remote replies is current sone`() { + val post = mockPostFrom(remoteSone) + val replies = listOf(mockReplyFrom(remoteSone), mockReplyFrom(remoteSone)) + whenever(core.getReplies("post-id")).thenReturn(replies) + assertThat(accessor[templateContext, post, "replySone"], equalTo(currentSone)) + } + + @Test + fun `reply sone for remote post with remote and one local replies is sone of local reply`() { + val post = mockPostFrom(remoteSone) + val localSone = mockLocalSone() + val replies = listOf(mockReplyFrom(remoteSone), mockReplyFrom(localSone)) + whenever(core.getReplies("post-id")).thenReturn(replies) + assertThat(accessor[templateContext, post, "replySone"], equalTo(localSone)) + } + + @Test + fun `reply sone for remote post with remote and several local replies is sone of latest local reply`() { + val post = mockPostFrom(remoteSone) + val localSone1 = mockLocalSone() + val localSone2 = mockLocalSone() + val replies = listOf(mockReplyFrom(remoteSone), mockReplyFrom(localSone1), mockReplyFrom(localSone2)) + whenever(core.getReplies("post-id")).thenReturn(replies) + assertThat(accessor[templateContext, post, "replySone"], equalTo(localSone2)) + } + + @Test + fun `reply sone for local post without replies is post sone`() { + val localSone = mockLocalSone() + val post = mockPostFrom(localSone) + assertThat(accessor[templateContext, post, "replySone"], equalTo(localSone)) + } + + @Test + fun `reply sone for local post with remote replies is local sone`() { + val localSone = mockLocalSone() + val post = mockPostFrom(localSone) + val replies = listOf(mockReplyFrom(remoteSone), mockReplyFrom(remoteSone)) + whenever(core.getReplies("post-id")).thenReturn(replies) + assertThat(accessor[templateContext, post, "replySone"], equalTo(localSone)) + } + + @Test + fun `reply sone for local post with remote and one local replies is local reply sone`() { + val localSone1 = mockLocalSone() + val post = mockPostFrom(localSone1) + val localSone2 = mockLocalSone() + val replies = listOf(mockReplyFrom(remoteSone), mockReplyFrom(localSone2)) + whenever(core.getReplies("post-id")).thenReturn(replies) + assertThat(accessor[templateContext, post, "replySone"], equalTo(localSone2)) + } + + @Test + fun `reply sone for local post with remote and several local replies is latest local reply sone`() { + val localSone1 = mockLocalSone() + val post = mockPostFrom(localSone1) + val localSone2 = mockLocalSone() + val localSone3 = mockLocalSone() + val replies = listOf(mockReplyFrom(remoteSone), mockReplyFrom(localSone2), mockReplyFrom(localSone3)) + whenever(core.getReplies("post-id")).thenReturn(replies) + assertThat(accessor[templateContext, post, "replySone"], equalTo(localSone3)) + } + + @Test + fun `accessor returns other properties`() { + assertThat(accessor[null, post, "hashCode"], equalTo(post.hashCode())) + } + +} + +private val currentSone = mock() +private val remoteSone = mock() +private fun mockLocalSone() = mock().apply { whenever(isLocal).thenReturn(true) } + +private val templateContext = TemplateContext().apply { + this["currentSone"] = currentSone +} + +private fun mockPostFrom(sone: Sone) = mock().apply { + whenever(id).thenReturn("post-id") + whenever(this.sone).thenReturn(sone) +} + +private fun mockReplyFrom(sone: Sone) = mock().apply { whenever(this.sone).thenReturn(sone) }