Merge branch 'paginate-moderation-log' into 'master'

Add pagination to moderation logs

See merge request framasoft/mobilizon!914
This commit is contained in:
Thomas Citharel 2021-04-28 09:01:03 +00:00
commit 65ea6325cd
14 changed files with 283 additions and 126 deletions

View file

@ -5,17 +5,50 @@
"kind": "INTERFACE",
"name": "ActionLogObject",
"possibleTypes": [
{
"name": "Comment"
},
{
"name": "Event"
},
{
"name": "Comment"
"name": "Person"
},
{
"name": "Report"
},
{
"name": "ReportNote"
},
{
"name": "User"
}
]
},
{
"kind": "INTERFACE",
"name": "ActivityObject",
"possibleTypes": [
{
"name": "Comment"
},
{
"name": "Discussion"
},
{
"name": "Event"
},
{
"name": "Group"
},
{
"name": "Member"
},
{
"name": "Post"
},
{
"name": "Resource"
}
]
},
@ -33,6 +66,18 @@
"name": "Application"
}
]
},
{
"kind": "INTERFACE",
"name": "Interactable",
"possibleTypes": [
{
"name": "Event"
},
{
"name": "Group"
}
]
}
]
}

View file

@ -6,28 +6,10 @@ import { AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN } from "@/constants";
import { REFRESH_TOKEN } from "@/graphql/auth";
import { saveTokenData } from "@/utils/auth";
import { ApolloClient } from "apollo-client";
import introspectionQueryResultData from "../../fragmentTypes.json";
export const fragmentMatcher = new IntrospectionFragmentMatcher({
introspectionQueryResultData: {
__schema: {
types: [
{
kind: "UNION",
name: "SearchResult",
possibleTypes: [
{ name: "Event" },
{ name: "Person" },
{ name: "Group" },
],
},
{
kind: "INTERFACE",
name: "Actor",
possibleTypes: [{ name: "Person" }, { name: "Group" }],
},
],
},
},
introspectionQueryResultData,
});
export async function refreshAccessToken(

View file

@ -158,8 +158,9 @@ export const CREATE_REPORT_NOTE = gql`
`;
export const LOGS = gql`
query {
actionLogs {
query ActionLogs($page: Int, $limit: Int) {
actionLogs(page: $page, limit: $limit) {
elements {
id
action
actor {
@ -190,6 +191,12 @@ export const LOGS = gql`
domain
name
}
... on Group {
id
preferredUsername
domain
name
}
... on User {
id
email
@ -198,5 +205,7 @@ export const LOGS = gql`
}
insertedAt
}
total
}
}
`;

View file

@ -985,5 +985,7 @@
"Unable to update the profile. The avatar picture may be too heavy.": "Unable to update the profile. The avatar picture may be too heavy.",
"Unable to create the profile. The avatar picture may be too heavy.": "Unable to create the profile. The avatar picture may be too heavy.",
"Error while loading the preview": "Error while loading the preview",
"Instance feeds": "Instance feeds"
"Instance feeds": "Instance feeds",
"{moderator} suspended group {profile}": "{moderator} suspended group {profile}",
"{moderator} has unsuspended group {profile}": "{moderator} has unsuspended group {profile}"
}

View file

@ -1079,5 +1079,7 @@
"Unable to update the profile. The avatar picture may be too heavy.": "Impossible de mettre à jour le profil. L'image d'avatar est probablement trop lourde.",
"Unable to create the profile. The avatar picture may be too heavy.": "Impossible de créer le profil. L'image d'avatar est probablement trop lourde.",
"Error while loading the preview": "Erreur lors du chargement de l'aperçu",
"Instance feeds": "Flux de l'instance"
"Instance feeds": "Flux de l'instance",
"{moderator} suspended group {profile}": "{moderator} a suspendu le groupe {profile}",
"{moderator} has unsuspended group {profile}": "{moderator} a annulé la suspension du groupe {profile}"
}

View file

@ -14,9 +14,9 @@
</li>
</ul>
</nav>
<section>
<ul v-if="actionLogs.length > 0">
<li v-for="log in actionLogs" :key="log.id">
<section v-if="actionLogs.total > 0 && actionLogs.elements.length > 0">
<ul>
<li v-for="log in actionLogs.elements" :key="log.id">
<div class="box">
<img
class="image"
@ -147,7 +147,10 @@
<b slot="title">{{ log.object.title }}</b>
</i18n>
<i18n
v-else-if="log.action === ActionLogAction.ACTOR_SUSPENSION"
v-else-if="
log.action === ActionLogAction.ACTOR_SUSPENSION &&
log.object.__typename == 'Person'
"
tag="span"
path="{moderator} suspended profile {profile}"
>
@ -169,7 +172,10 @@
</router-link>
</i18n>
<i18n
v-else-if="log.action === ActionLogAction.ACTOR_UNSUSPENSION"
v-else-if="
log.action === ActionLogAction.ACTOR_UNSUSPENSION &&
log.object.__typename == 'Person'
"
tag="span"
path="{moderator} has unsuspended profile {profile}"
>
@ -190,6 +196,56 @@
>{{ displayNameAndUsername(log.object) }}
</router-link>
</i18n>
<i18n
v-else-if="
log.action === ActionLogAction.ACTOR_SUSPENSION &&
log.object.__typename == 'Group'
"
tag="span"
path="{moderator} suspended group {profile}"
>
<router-link
slot="moderator"
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id },
}"
>@{{ log.actor.preferredUsername }}</router-link
>
<router-link
slot="profile"
:to="{
name: RouteName.ADMIN_GROUP_PROFILE,
params: { id: log.object.id },
}"
>{{ displayNameAndUsername(log.object) }}
</router-link>
</i18n>
<i18n
v-else-if="
log.action === ActionLogAction.ACTOR_UNSUSPENSION &&
log.object.__typename == 'Group'
"
tag="span"
path="{moderator} has unsuspended group {profile}"
>
<router-link
slot="moderator"
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id },
}"
>@{{ log.actor.preferredUsername }}</router-link
>
<router-link
slot="profile"
:to="{
name: RouteName.ADMIN_GROUP_PROFILE,
params: { id: log.object.id },
}"
>{{ displayNameAndUsername(log.object) }}
</router-link>
</i18n>
<i18n
v-else-if="log.action === ActionLogAction.USER_DELETION"
tag="span"
@ -219,20 +275,31 @@
</div>
</li>
</ul>
<b-pagination
:total="actionLogs.total"
v-model="page"
:per-page="LOGS_PER_PAGE"
:aria-next-label="$t('Next page')"
:aria-previous-label="$t('Previous page')"
:aria-page-label="$t('Page')"
:aria-current-label="$t('Current page')"
>
</b-pagination>
</section>
<div v-else>
<b-message type="is-info">{{ $t("No moderation logs yet") }}</b-message>
</div>
</section>
</div>
</template>
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import { Component, Vue, Watch } from "vue-property-decorator";
import { IActionLog } from "@/types/report.model";
import { LOGS } from "@/graphql/report";
import ReportCard from "@/components/Report/ReportCard.vue";
import { ActionLogAction } from "@/types/enums";
import RouteName from "../../router/name";
import { displayNameAndUsername } from "../../types/actor";
import { Paginate } from "@/types/paginate";
@Component({
components: {
@ -242,17 +309,39 @@ import { displayNameAndUsername } from "../../types/actor";
actionLogs: {
fetchPolicy: "cache-and-network",
query: LOGS,
variables() {
return {
page: this.page,
limit: this.LOGS_PER_PAGE,
};
},
},
},
})
export default class ReportList extends Vue {
actionLogs?: IActionLog[] = [];
actionLogs?: Paginate<IActionLog> = { total: 0, elements: [] };
page = parseInt((this.$route.query.page as string) || "1", 10);
LOGS_PER_PAGE = 10;
ActionLogAction = ActionLogAction;
RouteName = RouteName;
displayNameAndUsername = displayNameAndUsername;
mounted(): void {
this.page = parseInt((this.$route.query.page as string) || "1", 10);
}
@Watch("page")
triggerLoadMoreMemberPageChange(page: string): void {
this.$router.replace({
name: RouteName.REPORT_LOGS,
query: { ...this.$route.query, page },
});
}
}
</script>
<style lang="scss" scoped>
@ -265,4 +354,8 @@ img.image {
a {
text-decoration: none;
}
section ul li {
margin: 0.5rem auto;
}
</style>

View file

@ -27,7 +27,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
%{context: %{current_user: %User{role: role}}}
)
when is_moderator(role) do
with action_logs <- Mobilizon.Admin.list_action_logs(page, limit) do
with %Page{elements: action_logs, total: total} <-
Mobilizon.Admin.list_action_logs(page, limit) do
action_logs =
action_logs
|> Enum.map(fn %ActionLog{
@ -44,7 +45,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
end)
|> Enum.filter(& &1)
{:ok, action_logs}
{:ok, %Page{elements: action_logs, total: total}}
end
end

View file

@ -29,7 +29,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
Represents a group of actors
"""
object :group do
interfaces([:actor, :interactable, :activity_object])
interfaces([:actor, :interactable, :activity_object, :action_log_object])
field(:id, :id, description: "Internal ID for this group")
field(:url, :string, description: "The ActivityPub actor's URL")

View file

@ -22,6 +22,14 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
field(:inserted_at, :datetime, description: "The time when the action was performed")
end
@desc """
A paginated list of action logs
"""
object :paginated_action_log_list do
field(:elements, list_of(:action_log), description: "A list of action logs")
field(:total, :integer, description: "The total number of action logs in the list")
end
@desc """
The different types of action log actions
"""
@ -62,6 +70,9 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
%User{}, _ ->
:user
%Actor{type: "Group"}, _ ->
:group
_, _ ->
nil
end)
@ -144,7 +155,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
object :admin_queries do
@desc "Get the list of action logs"
field :action_logs, type: list_of(:action_log) do
field :action_logs, type: :paginated_action_log_list do
arg(:page, :integer, default_value: 1)
arg(:limit, :integer, default_value: 10)
resolve(&Admin.list_action_logs/3)

View file

@ -36,11 +36,10 @@ defmodule Mobilizon.Admin do
@doc """
Returns the list of action logs.
"""
@spec list_action_logs(integer | nil, integer | nil) :: [ActionLog.t()]
@spec list_action_logs(integer | nil, integer | nil) :: Page.t()
def list_action_logs(page \\ nil, limit \\ nil) do
list_action_logs_query()
|> Page.paginate(page, limit)
|> Repo.all()
|> Page.build_page(page, limit)
end
@doc """

View file

@ -31,6 +31,8 @@ defmodule Mobilizon.GraphQL.Resolvers.AdminTest do
query = """
{
actionLogs {
total
elements {
action,
actor {
preferredUsername
@ -46,6 +48,7 @@ defmodule Mobilizon.GraphQL.Resolvers.AdminTest do
}
}
}
}
"""
res =
@ -62,9 +65,10 @@ defmodule Mobilizon.GraphQL.Resolvers.AdminTest do
assert json_response(res, 200)["errors"] == nil
assert json_response(res, 200)["data"]["actionLogs"] |> length == 3
assert json_response(res, 200)["data"]["actionLogs"]["total"] == 3
assert json_response(res, 200)["data"]["actionLogs"]["elements"] |> length == 3
assert json_response(res, 200)["data"]["actionLogs"] == [
assert json_response(res, 200)["data"]["actionLogs"]["elements"] == [
%{
"action" => "NOTE_DELETION",
"actor" => %{"preferredUsername" => moderator_2.preferred_username},

View file

@ -228,6 +228,8 @@ defmodule Mobilizon.GraphQL.Resolvers.CommentTest do
query = """
{
actionLogs {
total
elements {
action,
actor {
preferredUsername
@ -251,6 +253,7 @@ defmodule Mobilizon.GraphQL.Resolvers.CommentTest do
}
}
}
}
"""
res =
@ -260,7 +263,7 @@ defmodule Mobilizon.GraphQL.Resolvers.CommentTest do
refute json_response(res, 200)["errors"]
assert hd(json_response(res, 200)["data"]["actionLogs"]) == %{
assert hd(json_response(res, 200)["data"]["actionLogs"]["elements"]) == %{
"action" => "COMMENT_DELETION",
"actor" => %{"preferredUsername" => actor_moderator.preferred_username},
"object" => %{"text" => comment.text, "id" => to_string(comment.id)}

View file

@ -1368,6 +1368,8 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
query = """
{
actionLogs {
total
elements {
action,
actor {
preferredUsername
@ -1387,6 +1389,7 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
}
}
}
}
"""
res =
@ -1394,7 +1397,7 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
|> auth_conn(user_moderator)
|> get("/api", AbsintheHelpers.query_skeleton(query, "actionLogs"))
assert hd(json_response(res, 200)["data"]["actionLogs"]) == %{
assert hd(json_response(res, 200)["data"]["actionLogs"]["elements"]) == %{
"action" => "EVENT_DELETION",
"actor" => %{"preferredUsername" => actor_moderator.preferred_username},
"object" => %{"title" => event.title, "id" => to_string(event.id)}

View file

@ -685,6 +685,8 @@ defmodule Mobilizon.GraphQL.Resolvers.PersonTest do
@moderation_logs_query """
{
actionLogs {
total
elements {
action,
actor {
id,
@ -698,6 +700,7 @@ defmodule Mobilizon.GraphQL.Resolvers.PersonTest do
}
}
}
}
"""
test "suspends a remote profile", %{conn: conn} do
@ -733,7 +736,7 @@ defmodule Mobilizon.GraphQL.Resolvers.PersonTest do
|> auth_conn(modo)
|> AbsintheHelpers.graphql_query(query: @moderation_logs_query)
actionlog = hd(res["data"]["actionLogs"])
actionlog = hd(res["data"]["actionLogs"]["elements"])
refute is_nil(actionlog)
assert actionlog["action"] == "ACTOR_SUSPENSION"
assert actionlog["actor"]["id"] == to_string(modo_actor_id)