Add search for video, reporter and channel name fields
authorRigel Kent <sendmemail@rigelk.eu>
Sat, 18 Apr 2020 08:00:19 +0000 (10:00 +0200)
committerRigel Kent <par@rigelk.eu>
Fri, 1 May 2020 14:41:02 +0000 (16:41 +0200)
client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html
client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss
client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts
client/src/app/shared/video-abuse/video-abuse.service.ts
server/controllers/api/videos/abuse.ts
server/models/video/video-abuse.ts

index 2204bb371c5ffb0ded2bc7c4fb51d1d514404fb3..c1ce093d7d3aeaaba8d35eb60ec552041d9b1cb5 100644 (file)
@@ -4,6 +4,17 @@
   [showCurrentPageReport]="true" i18n-currentPageReportTemplate
   currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} reports"
 >
+  <ng-template pTemplate="caption">
+    <div class="caption">
+      <div class="ml-auto">
+        <input
+          type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
+          (keyup)="onSearch($event)"
+        >
+      </div>
+    </div>
+  </ng-template>
+
   <ng-template pTemplate="header">
     <tr> <!-- header -->
       <th style="width: 40px;"></th>
         </a>
       </td>
 
-      <td>
+      <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 *ngIf="!videoAbuse.video.deleted" [src]="videoAbuse.video.thumbnailPath">
-              <span *ngIf="videoAbuse.video.deleted" i18n>Deleted</span>
-            </div>
+            <div class="video-abuse-video-image"><img [src]="videoAbuse.video.thumbnailPath"></div>
             <div class="video-abuse-video-text">
               <div>
                 {{ videoAbuse.video.name }}
-                <span *ngIf="!videoAbuse.video.deleted && !videoAbuse.video.blacklisted" class="glyphicon glyphicon-new-window"></span>
-                <span *ngIf="videoAbuse.video.deleted" i18n-title title="Video was deleted" class="glyphicon glyphicon-trash"></span>
+                <span *ngIf="!videoAbuse.video.blacklisted" class="glyphicon glyphicon-new-window"></span>
                 <span *ngIf="videoAbuse.video.blacklisted" i18n-title title="Video was blacklisted" class="glyphicon glyphicon-ban-circle"></span>
               </div>
               <div class="text-muted">by {{ videoAbuse.video.channel?.displayName }} on {{ videoAbuse.video.channel?.host }} </div>
         </a>
       </td>
 
-      <td>{{ videoAbuse.createdAt }}</td>
+      <td *ngIf="videoAbuse.video.deleted" class="c-hand" [pRowToggler]="videoAbuse">
+        <div class="video-abuse-video" i18n-title title="Video was deleted">
+          <div class="video-abuse-video-image"><span i18n>Deleted</span></div>
+          <div class="video-abuse-video-text">
+            <div>
+              {{ videoAbuse.video.name }}
+              <span class="glyphicon glyphicon-trash"></span>
+            </div>
+            <div class="text-muted">by {{ videoAbuse.video.channel?.displayName }} on {{ videoAbuse.video.channel?.host }} </div>
+          </div>
+        </div>
+      </td>
+
+      <td class="c-hand" [pRowToggler]="videoAbuse">{{ videoAbuse.createdAt }}</td>
 
       <td class="c-hand video-abuse-states" [pRowToggler]="videoAbuse">
         <span *ngIf="isVideoAbuseAccepted(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-ok"></span>
index 09402fda7e270bf20bcd0f1762bb158c904460f4..9b60c39dc08c87556514f266c663909b91870ea5 100644 (file)
@@ -1,6 +1,14 @@
 @import 'mixins';
 @import 'miniature';
 
+.caption {
+  justify-content: flex-end;
+
+  input {
+    @include peertube-input-text(250px);
+  }
+}
+
 .video-abuse-video-link {
   @include disable-outline;
   position: relative;
index cc5014ae8596bbd8b211320608d5cf16ecb864a6..6dcf96ccfe02f5ae588ae185ac4cfaa8fec9d6ba 100644 (file)
@@ -16,6 +16,8 @@ import { getAbsoluteAPIUrl } from '@app/shared/misc/utils'
 import { DomSanitizer } from '@angular/platform-browser'
 import { BlocklistService } from '@app/shared/blocklist'
 import { VideoService } from '@app/shared/video/video.service'
+import { ActivatedRoute } from '@angular/router'
+import { first } from 'rxjs/operators'
 
 @Component({
   selector: 'my-video-abuse-list',
@@ -43,7 +45,8 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
     private confirmService: ConfirmService,
     private i18n: I18n,
     private markdownRenderer: MarkdownService,
-    private sanitizer: DomSanitizer
+    private sanitizer: DomSanitizer,
+    private route: ActivatedRoute,
   ) {
     super()
 
@@ -185,6 +188,10 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
 
   ngOnInit () {
     this.initialize()
+
+    this.route.queryParams
+      .pipe(first(params => params.search !== undefined && params.search !== null))
+      .subscribe(params => this.search = params.search)
   }
 
   getIdentifier () {
@@ -253,26 +260,29 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
   }
 
   protected loadData () {
-    return this.videoAbuseService.getVideoAbuses(this.pagination, this.sort)
-               .subscribe(
-                 async resultList => {
-                   this.totalRecords = resultList.total
-
-                   this.videoAbuses = resultList.data
-
-                   for (const abuse of this.videoAbuses) {
-                     Object.assign(abuse, {
-                       reasonHtml: await this.toHtml(abuse.reason),
-                       moderationCommentHtml: await this.toHtml(abuse.moderationComment),
-                       embedHtml: this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(abuse)),
-                       reporterAccount: new Account(abuse.reporterAccount)
-                     })
-                   }
-
-                 },
-
-                 err => this.notifier.error(err.message)
-               )
+    return this.videoAbuseService.getVideoAbuses({
+      pagination: this.pagination,
+      sort: this.sort,
+      search: this.search
+    }).subscribe(
+        async resultList => {
+          this.totalRecords = resultList.total
+
+          this.videoAbuses = resultList.data
+
+          for (const abuse of this.videoAbuses) {
+            Object.assign(abuse, {
+              reasonHtml: await this.toHtml(abuse.reason),
+              moderationCommentHtml: await this.toHtml(abuse.moderationComment),
+              embedHtml: this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(abuse)),
+              reporterAccount: new Account(abuse.reporterAccount)
+            })
+          }
+
+        },
+
+        err => this.notifier.error(err.message)
+      )
   }
 
   private toHtml (text: string) {
index 61a32857567603a2bfb67071afe4da62b8453bda..a39ad31d491ee897da6af579f563ce336c95a0e5 100644 (file)
@@ -17,12 +17,19 @@ export class VideoAbuseService {
     private restExtractor: RestExtractor
   ) {}
 
-  getVideoAbuses (pagination: RestPagination, sort: SortMeta): Observable<ResultList<VideoAbuse>> {
+  getVideoAbuses (options: {
+    pagination: RestPagination,
+    sort: SortMeta,
+    search?: string
+  }): Observable<ResultList<VideoAbuse>> {
+    const { pagination, sort, search } = options
     const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + 'abuse'
 
     let params = new HttpParams()
     params = this.restService.addRestGetParams(params, pagination, sort)
 
+    if (search) params = params.append('search', search)
+
     return this.authHttp.get<ResultList<VideoAbuse>>(url, { params })
                .pipe(
                  map(res => this.restExtractor.convertResultListDateToHuman(res)),
index 4ae899b7e7e91f5e64ed18ab169e60fb28bc5334..f37d908969f0daf678315f7629982efa9903fb6c 100644 (file)
@@ -69,6 +69,7 @@ async function listVideoAbuses (req: express.Request, res: express.Response) {
     start: req.query.start,
     count: req.query.count,
     sort: req.query.sort,
+    search: req.query.search,
     serverAccountId: serverActor.Account.id,
     user
   })
index ea943ffdfe4202a0a4ccb103d9be706f1de69a1d..5ead02ecae5d46b446b1a18d2be99e2848ddac7e 100644 (file)
@@ -1,5 +1,5 @@
 import {
-  AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt, DefaultScope
+  AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt, Scopes
 } from 'sequelize-typescript'
 import { VideoAbuseObject } from '../../../shared/models/activitypub/objects'
 import { VideoAbuse } from '../../../shared/models/videos'
@@ -21,34 +21,99 @@ import { VideoChannelModel } from './video-channel'
 import { ActorModel } from '../activitypub/actor'
 import { VideoBlacklistModel } from './video-blacklist'
 
-@DefaultScope(() => ({
-  include: [
-    {
-      model: AccountModel,
-      required: true
-    },
-    {
-      model: VideoModel,
-      required: false,
+export enum ScopeNames {
+  FOR_API = 'FOR_API'
+}
+
+@Scopes(() => ({
+  [ScopeNames.FOR_API]: (options: {
+    search?: string
+    searchReporter?: string
+    searchVideo?: string
+    searchVideoChannel?: string
+    serverAccountId: number
+    userAccountId: any
+  }) => {
+    const search = (sourceField, targetField) => sourceField ? ({
+      [targetField]: {
+        [Op.iLike]: `%${sourceField}%`
+      }
+    }) : {}
+
+    let where = {
+      reporterAccountId: {
+        [Op.notIn]: literal('(' + buildBlockedAccountSQL(options.serverAccountId, options.userAccountId) + ')')
+      }
+    }
+
+    if (options.search) {
+      where = Object.assign(where, {
+        [Op.or]: [
+          {
+            [Op.and]: [
+              { videoId: { [Op.not]: null } },
+              { '$Video.name$': { [Op.iLike]: `%${options.search}%` } }
+            ]
+          },
+          {
+            [Op.and]: [
+              { videoId: { [Op.not]: null } },
+              { '$Video.VideoChannel.name$': { [Op.iLike]: `%${options.search}%` } }
+            ]
+          },
+          {
+            [Op.and]: [
+              { deletedVideo: { [Op.not]: null } },
+              { deletedVideo: { name: { [Op.iLike]: `%${options.search}%` } } }
+            ]
+          },
+          {
+            [Op.and]: [
+              { deletedVideo: { [Op.not]: null } },
+              { deletedVideo: { channel: { displayName: { [Op.iLike]: `%${options.search}%` } } } }
+            ]
+          },
+          { '$Account.name$': { [Op.iLike]: `%${options.search}%` } }
+        ]
+      })
+    }
+
+    console.log(where)
+
+    return {
       include: [
         {
-          model: ThumbnailModel
+          model: AccountModel,
+          required: true,
+          where: { ...search(options.searchReporter, 'name') }
         },
         {
-          model: VideoChannelModel.unscoped(),
+          model: VideoModel,
+          required: false,
+          where: { ...search(options.searchVideo, 'name') },
           include: [
             {
-              model: ActorModel
+              model: ThumbnailModel
+            },
+            {
+              model: VideoChannelModel.unscoped(),
+              where: { ...search(options.searchVideoChannel, 'name') },
+              include: [
+                {
+                  model: ActorModel
+                }
+              ]
+            },
+            {
+              attributes: [ 'id', 'reason', 'unfederated' ],
+              model: VideoBlacklistModel
             }
           ]
-        },
-        {
-          attributes: [ 'id', 'reason', 'unfederated' ],
-          model: VideoBlacklistModel
         }
-      ]
+      ],
+      where
     }
-  ]
+  }
 }))
 @Table({
   tableName: 'videoAbuse',
@@ -134,26 +199,30 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
     start: number
     count: number
     sort: string
+    search?: string
     serverAccountId: number
     user?: MUserAccountId
   }) {
-    const { start, count, sort, user, serverAccountId } = parameters
+    const { start, count, sort, search, user, serverAccountId } = parameters
     const userAccountId = user ? user.Account.id : undefined
 
     const query = {
       offset: start,
       limit: count,
       order: getSort(sort),
-      where: {
-        reporterAccountId: {
-          [Op.notIn]: literal('(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')')
-        }
-      },
       col: 'VideoAbuseModel.id',
       distinct: true
     }
 
-    return VideoAbuseModel.findAndCountAll(query)
+    const filters = {
+      search,
+      serverAccountId,
+      userAccountId
+    }
+
+    return VideoAbuseModel
+      .scope({ method: [ ScopeNames.FOR_API, filters ] })
+      .findAndCountAll(query)
       .then(({ rows, count }) => {
         return { total: count, data: rows }
       })