Add nth abuse count for a given video, add reporter/reportee reports stats

pull/2711/head
Rigel Kent 2020-04-18 22:57:20 +02:00 committed by Rigel Kent
parent 844db39ee5
commit 5fd4ca0051
7 changed files with 206 additions and 39 deletions

View File

@ -7,19 +7,23 @@
margin-right: 30px;
}
.moderation-expanded-label {
font-weight: $font-semibold;
display: inline-block;
vertical-align: top;
text-align: right;
}
.moderation-expanded {
font-size: 90%;
.moderation-expanded-text {
display: inline-block;
word-wrap: break-word;
::ng-deep p:last-child {
margin-bottom: 0px !important;
.moderation-expanded-label {
font-weight: $font-semibold;
display: inline-block;
vertical-align: top;
text-align: right;
}
.moderation-expanded-text {
display: inline-flex;
word-wrap: break-word;
::ng-deep p:last-child {
margin-bottom: 0px !important;
}
}
}
@ -58,3 +62,9 @@
.chip {
@include chip;
}
my-action-dropdown.show {
::ng-deep .dropdown-root {
display: block !important;
}
}

View File

@ -54,7 +54,13 @@
<td *ngIf="!videoAbuse.video.deleted">
<a [href]="getVideoUrl(videoAbuse)" class="video-abuse-video-link" i18n-title title="Open video in a new tab" target="_blank" rel="noopener noreferrer">
<div class="video-abuse-video">
<div class="video-abuse-video-image"><img [src]="videoAbuse.video.thumbnailPath"></div>
<div class="video-abuse-video-image">
<img [src]="videoAbuse.video.thumbnailPath">
<span
class="video-abuse-video-image-label" *ngIf="videoAbuse.count > 1"
i18n-title title="This video has been reported multiple times."
>{{ videoAbuse.nth }}/{{ videoAbuse.count }}</span>
</div>
<div class="video-abuse-video-text">
<div>
{{ videoAbuse.video.name }}
@ -85,11 +91,14 @@
<td class="c-hand video-abuse-states" [pRowToggler]="videoAbuse">
<span *ngIf="isVideoAbuseAccepted(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-ok"></span>
<span *ngIf="isVideoAbuseRejected(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-remove"></span>
<span *ngIf="videoAbuse.moderationComment" [title]="videoAbuse.moderationComment" class="glyphicon glyphicon-comment"></span>
<span *ngIf="videoAbuse.moderationComment" container="body" placement="left auto" [ngbTooltip]="videoAbuse.moderationComment" class="glyphicon glyphicon-comment"></span>
</td>
<td class="action-cell">
<my-action-dropdown placement="bottom-right auto" container="body" i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse"></my-action-dropdown>
<my-action-dropdown
[ngClass]="{ 'show': expanded }" placement="bottom-right auto" container="body"
i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse"
></my-action-dropdown>
</td>
</tr>
</ng-template>
@ -97,14 +106,59 @@
<ng-template pTemplate="rowexpansion" let-videoAbuse>
<tr>
<td class="expand-cell" colspan="6">
<div class="d-flex">
<div class="d-flex moderation-expanded">
<!-- report metadata -->
<div class="col-8">
<div class="d-flex">
<span class="col-3 moderation-expanded-label" i18n>Reason:</span>
<span class="col-3 moderation-expanded-label" i18n>Reporter</span>
<span class="col-9 moderation-expanded-text">
<div class="chip">
<img
class="avatar"
[src]="videoAbuse.reporterAccount.avatar.path"
(error)="switchToDefaultAvatar($event)"
alt="Avatar"
>
<div>
<span class="text-muted">{{ createByString(videoAbuse.reporterAccount) }}</span>
</div>
</div>
<a routerLink="/admin/moderation/video-abuses/list" class="ml-auto text-muted video-abuse-links" i18n>
{videoAbuse.countReportsForReporter, plural, =1 {1 report} other {{{ videoAbuse.countReportsForReporter }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
</a>
</span>
</div>
<div class="d-flex">
<span class="col-3 moderation-expanded-label" i18n>Reportee</span>
<span class="col-9 moderation-expanded-text">
<div class="chip">
<img
class="avatar"
[src]="videoAbuse.video.channel.ownerAccount?.avatar.path"
(error)="switchToDefaultAvatar($event)"
alt="Avatar"
>
<div>
<span class="text-muted">{{ videoAbuse.video.channel.ownerAccount ? createByString(videoAbuse.video.channel.ownerAccount) : '' }}</span>
</div>
</div>
<a routerLink="/admin/moderation/video-abuses/list" class="ml-auto text-muted video-abuse-links" *ngIf="!videoAbuse.video.deleted" i18n>
{videoAbuse.countReportsForReportee, plural, =1 {1 report} other {{{ videoAbuse.countReportsForReportee }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
</a>
</span>
</div>
<div class="d-flex">
<span class="col-3 moderation-expanded-label" i18n>Updated</span>
<time class="col-9 moderation-expanded-text video-abuse-date-updated">{{ videoAbuse.updatedAt | date: 'medium' }}</time>
</div>
<!-- report text -->
<div class="mt-3 d-flex">
<span class="col-3 moderation-expanded-label" i18n>Report</span>
<span class="col-9 moderation-expanded-text" [innerHTML]="videoAbuse.reasonHtml"></span>
</div>
<div class="mt-3 d-flex" *ngIf="videoAbuse.moderationComment">
<span class="col-3 moderation-expanded-label" i18n>Note:</span>
<span class="col-3 moderation-expanded-label" i18n>Note</span>
<span class="col-9 moderation-expanded-text" [innerHTML]="videoAbuse.moderationCommentHtml"></span>
</div>
</div>

View File

@ -9,6 +9,15 @@
}
}
.video-abuse-date-updated {
font-size: 90%;
margin-top: .1rem;
}
.video-abuse-links {
@include disable-default-a-behaviour;
}
.video-abuse-video-link {
@include disable-outline;
position: relative;
@ -32,6 +41,7 @@
display: inline-flex;
justify-content: center;
align-items: center;
position: relative;
img {
height: 100%;
@ -42,6 +52,17 @@
span {
color: var(--inputPlaceholderColor);
}
.video-abuse-video-image-label {
@include static-thumbnail-overlay;
position: absolute;
border-radius: 3px;
font-size: 10px;
padding: 0 3px;
line-height: 1.3;
bottom: 2px;
right: 2px;
}
}
.video-abuse-video-text {

View File

@ -46,7 +46,7 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
private i18n: I18n,
private markdownRenderer: MarkdownService,
private sanitizer: DomSanitizer,
private route: ActivatedRoute,
private route: ActivatedRoute
) {
super()
@ -223,7 +223,7 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
}
getVideoEmbed (videoAbuse: VideoAbuse) {
const absoluteAPIUrl = 'http://localhost:9000' || getAbsoluteAPIUrl()
const absoluteAPIUrl = 'http://localhost:9000' || getAbsoluteAPIUrl() // TODO
const embedUrl = buildVideoLink({
baseUrl: absoluteAPIUrl + '/videos/embed/' + videoAbuse.video.uuid,
warningTitle: false

View File

@ -30,9 +30,16 @@
</a>
</td>
<td>{{ booleanToText(videoBlacklist.video.nsfw) }}</td>
<td>{{ booleanToText(videoBlacklist.unfederated) }}</td>
<td>{{ videoBlacklist.createdAt }}</td>
<ng-container *ngIf="videoBlacklist.reason">
<td class="c-hand" [pRowToggler]="videoBlacklist">{{ booleanToText(videoBlacklist.video.nsfw) }}</td>
<td class="c-hand" [pRowToggler]="videoBlacklist">{{ booleanToText(videoBlacklist.unfederated) }}</td>
<td class="c-hand" [pRowToggler]="videoBlacklist">{{ videoBlacklist.createdAt }}</td>
</ng-container>
<ng-container *ngIf="!videoBlacklist.reason">
<td>{{ booleanToText(videoBlacklist.video.nsfw) }}</td>
<td>{{ booleanToText(videoBlacklist.unfederated) }}</td>
<td>{{ videoBlacklist.createdAt }}</td>
</ng-container>
<td class="action-cell">
<my-action-dropdown i18n-label placement="bottom-right" label="Actions" [actions]="videoBlacklistActions" [entry]="videoBlacklist"></my-action-dropdown>

View File

@ -11,14 +11,13 @@ import {
import { AccountModel } from '../account/account'
import { buildBlockedAccountSQL, getSort, throwIfNotValid } from '../utils'
import { VideoModel } from './video'
import { VideoAbuseState, Video } from '../../../shared'
import { VideoAbuseState, VideoDetails } from '../../../shared'
import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants'
import { MUserAccountId, MVideoAbuse, MVideoAbuseFormattable, MVideoAbuseVideo } from '../../typings/models'
import * as Bluebird from 'bluebird'
import { literal, Op } from 'sequelize'
import { ThumbnailModel } from './thumbnail'
import { VideoChannelModel } from './video-channel'
import { ActorModel } from '../activitypub/actor'
import { VideoBlacklistModel } from './video-blacklist'
export enum ScopeNames {
@ -78,9 +77,73 @@ export enum ScopeNames {
})
}
console.log(where)
return {
attributes: {
include: [
[
literal(
'(' +
'SELECT t.count ' +
'FROM ( ' +
'SELECT id, ' +
'count(id) OVER (PARTITION BY "videoId") ' +
'FROM "videoAbuse" ' +
') t ' +
'WHERE t.id = "VideoAbuseModel".id ' +
')'
),
'countReportsForVideo'
],
[
literal(
'(' +
'SELECT t.nth ' +
'FROM ( ' +
'SELECT id, ' +
'row_number() OVER (PARTITION BY "videoId" ORDER BY "createdAt") AS nth ' +
'FROM "videoAbuse" ' +
') t ' +
'WHERE t.id = "VideoAbuseModel".id ' +
')'
),
'nthReportForVideo'
],
[
literal(
'(' +
'SELECT count("videoAbuse"."id") ' +
'FROM "videoAbuse" ' +
'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' +
'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
'WHERE "account"."id" = "VideoAbuseModel"."reporterAccountId" ' +
')'
),
'countReportsForReporter'
],
[
literal(
'(' +
'WITH ' +
'ids AS ( ' +
'SELECT "account"."id" ' +
'FROM "account" ' +
'INNER JOIN "videoChannel" ON "videoChannel"."accountId" = "account"."id" ' +
'INNER JOIN "video" ON "video"."channelId" = "videoChannel"."id" ' +
'WHERE "video"."id" = "VideoAbuseModel"."videoId" ' +
') ' +
'SELECT count("videoAbuse"."id") ' +
'FROM "videoAbuse" ' +
'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' +
'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
'INNER JOIN ids ON "account"."id" = ids.id ' +
')'
),
'countReportsForReportee'
]
]
},
include: [
{
model: AccountModel,
@ -96,13 +159,8 @@ export enum ScopeNames {
model: ThumbnailModel
},
{
model: VideoChannelModel.unscoped(),
where: { ...search(options.searchVideoChannel, 'name') },
include: [
{
model: ActorModel
}
]
model: VideoChannelModel.scope([ 'WITH_ACTOR', 'WITH_ACCOUNT' ]),
where: { ...search(options.searchVideoChannel, 'name') }
},
{
attributes: [ 'id', 'reason', 'unfederated' ],
@ -149,7 +207,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
@AllowNull(true)
@Default(null)
@Column(DataType.JSONB)
deletedVideo: Video
deletedVideo: VideoDetails
@CreatedAt
createdAt: Date
@ -229,6 +287,11 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
}
toFormattedJSON (this: MVideoAbuseFormattable): VideoAbuse {
const countReportsForVideo = this.get('countReportsForVideo') as number
const nthReportForVideo = this.get('nthReportForVideo') as number
const countReportsForReporter = this.get('countReportsForReporter') as number
const countReportsForReportee = this.get('countReportsForReportee') as number
const video = this.Video
? this.Video
: this.deletedVideo
@ -250,9 +313,14 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
deleted: !this.Video,
blacklisted: this.Video && this.Video.isBlacklisted(),
thumbnailPath: this.Video?.getMiniatureStaticPath(),
channel: this.Video?.VideoChannel.toFormattedSummaryJSON() || this.deletedVideo?.channel
channel: this.Video?.VideoChannel.toFormattedJSON() || this.deletedVideo?.channel
},
createdAt: this.createdAt
createdAt: this.createdAt,
updatedAt: this.updatedAt,
count: countReportsForVideo || 0,
nth: nthReportForVideo || 0,
countReportsForReporter: countReportsForReporter || 0,
countReportsForReportee: countReportsForReportee || 0
}
}

View File

@ -1,7 +1,7 @@
import { Account } from '../../actors/index'
import { VideoConstant } from '../video-constant.model'
import { VideoAbuseState } from './video-abuse-state.model'
import { VideoChannelSummary } from '../channel/video-channel.model'
import { VideoChannel } from '../channel/video-channel.model'
export interface VideoAbuse {
id: number
@ -19,8 +19,15 @@ export interface VideoAbuse {
deleted: boolean
blacklisted: boolean
thumbnailPath?: string
channel?: VideoChannelSummary
channel?: VideoChannel
}
createdAt: Date
updatedAt: Date
count?: number
nth?: number
countReportsForReporter?: number
countReportsForReportee?: number
}