From 1eebaaf6d8706c059f9e930bac18caff0733e8bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Thu, 29 Aug 2024 10:32:11 +0200 Subject: [PATCH] Better admin action for listing moderation cases. (#7996) --- .../admin/actions/moderation_case_list.dart | 62 ++++++++++++++++--- app/lib/admin/models.dart | 20 +++++- app/test/admin/moderation_case_test.dart | 4 +- 3 files changed, 76 insertions(+), 10 deletions(-) diff --git a/app/lib/admin/actions/moderation_case_list.dart b/app/lib/admin/actions/moderation_case_list.dart index 98b18136c..5008ba40d 100644 --- a/app/lib/admin/actions/moderation_case_list.dart +++ b/app/lib/admin/actions/moderation_case_list.dart @@ -2,6 +2,8 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'package:_pub_shared/search/search_form.dart'; +import 'package:clock/clock.dart'; import 'package:pub_dev/admin/models.dart'; import 'package:pub_dev/shared/datastore.dart'; @@ -9,17 +11,30 @@ import 'actions.dart'; final moderationCaseList = AdminAction( name: 'moderation-case-list', - summary: 'List the currently active (or resolved) ModerationCase entities.', + summary: 'List ModerationCase entities.', description: ''' -List the ModerationCase entities with filter options. +List ModerationCase entities with filter options. ''', options: { 'sort': 'Sort by the given attribute: `opened` (default), `resolved`.', + 'status': '`pending` | `resolved` | ' + '${ModerationStatus.resolveValues.map((v) => '`$v`').join(' | ')}', + 'kind': '`appeal` | `notification`', + 'subject': 'The (substring of) the subject on the moderation case.', + 'density': '`caseIds` (default) | `compact` | `expanded`', + 'past': + 'Limit the results opened (or resolved depending on `sort`) using "2w" or other time ranges.' }, invoke: (options) async { final sort = options['sort'] ?? 'opened'; - final query = dbService.query()..limit(20); + final status = options['status']; + final kind = options['kind']; + final subject = options['subject']; + final density = options['density'] ?? 'caseIds'; + final past = options['past']; + final pastDuration = past == null ? null : parseTime(past); + final query = dbService.query(); switch (sort) { case 'opened': query.order('-opened'); @@ -31,9 +46,42 @@ List the ModerationCase entities with filter options. throw InvalidInputException('invalid sort value'); } - final list = await query.run().toList(); - return { - 'cases': list.map((e) => e.toDebugInfo()).toList(), - }; + final list = await query.run().where((mc) { + if (status == 'pending' && mc.status != ModerationStatus.pending) { + return false; + } + if (status == 'resolved' && mc.status == ModerationStatus.pending) { + return false; + } + if (kind != null && mc.kind != kind) { + return false; + } + if (subject != null && !mc.subject.contains(subject)) { + return false; + } + final now = clock.now(); + if (pastDuration != null) { + if (sort == 'opened' && now.difference(mc.opened) > pastDuration) { + return false; + } + if (sort == 'resolved' && + (mc.resolved == null || + now.difference(mc.resolved!) > pastDuration)) { + return false; + } + } + return true; + }).toList(); + + switch (density) { + case 'caseIds': + return {'caseIds': list.map((e) => e.caseId).toList()}; + case 'compact': + return {'cases': list.map((e) => e.toCompactInfo()).toList()}; + case 'expanded': + return {'cases': list.map((e) => e.toDebugInfo()).toList()}; + default: + throw InvalidInputException('invalid density value'); + } }, ); diff --git a/app/lib/admin/models.dart b/app/lib/admin/models.dart index 550c0bca0..177d56300 100644 --- a/app/lib/admin/models.dart +++ b/app/lib/admin/models.dart @@ -164,6 +164,18 @@ class ModerationCase extends db.ExpandoModel { setActionLog(log); } + Map toCompactInfo() { + return { + 'caseId': caseId, + 'reporterEmail': reporterEmail, + 'kind': kind, + 'opened': opened.toIso8601String(), + 'resolved': resolved?.toIso8601String(), + 'subject': subject, + if (appealedCaseId != null) 'appealedCaseId': appealedCaseId, + }; + } + Map toDebugInfo() { return { 'caseId': caseId, @@ -222,8 +234,7 @@ abstract class ModerationStatus { static const moderationUpheld = 'moderation-upheld'; static const moderationReverted = 'moderation-reverted'; - static const _values = [ - pending, + static const resolveValues = [ noAction, moderationApplied, noActionUpheld, @@ -231,6 +242,11 @@ abstract class ModerationStatus { moderationUpheld, moderationReverted, ]; + static const _values = [ + pending, + ...resolveValues, + ]; + static bool isValidStatus(String value) => _values.contains(value); static bool wasModerationApplied(String value) => value == ModerationStatus.moderationApplied || diff --git a/app/test/admin/moderation_case_test.dart b/app/test/admin/moderation_case_test.dart index 17f40b913..91caac71c 100644 --- a/app/test/admin/moderation_case_test.dart +++ b/app/test/admin/moderation_case_test.dart @@ -177,7 +177,9 @@ void main() { final list = await api.adminInvokeAction( 'moderation-case-list', - AdminInvokeActionArguments(arguments: {}), + AdminInvokeActionArguments(arguments: { + 'density': 'expanded', + }), ); expect(list.output, { 'cases': [