rewrite reports management page with React
This commit is contained in:
parent
c0d9d18efc
commit
291efe730f
@ -9,6 +9,7 @@ use Blessing\Filter;
|
|||||||
use Blessing\Rejection;
|
use Blessing\Rejection;
|
||||||
use Illuminate\Contracts\Events\Dispatcher;
|
use Illuminate\Contracts\Events\Dispatcher;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
class ReportController extends Controller
|
class ReportController extends Controller
|
||||||
@ -65,46 +66,35 @@ class ReportController extends Controller
|
|||||||
|
|
||||||
public function manage(Request $request)
|
public function manage(Request $request)
|
||||||
{
|
{
|
||||||
$search = $request->input('search', '');
|
$q = $request->input('q');
|
||||||
$sortField = $request->input('sortField', 'report_at');
|
|
||||||
$sortType = $request->input('sortType', 'desc');
|
|
||||||
$page = $request->input('page', 1);
|
|
||||||
$perPage = $request->input('perPage', 10);
|
|
||||||
|
|
||||||
$reports = Report::where('tid', 'like', '%'.$search.'%')
|
$pagination = Report::usingSearchString($q)->paginate(9);
|
||||||
->orWhere('reporter', 'like', '%'.$search.'%')
|
$collection = $pagination->getCollection()->map(function ($report) {
|
||||||
->orWhere('reason', 'like', '%'.$search.'%')
|
$uploader = User::find($report->uploader);
|
||||||
->orderBy($sortField, $sortType)
|
if ($uploader) {
|
||||||
->offset(($page - 1) * $perPage)
|
$report->uploaderName = $uploader->nickname;
|
||||||
->limit($perPage)
|
}
|
||||||
->get()
|
if ($report->informer) {
|
||||||
->makeHidden(['informer'])
|
$report->reporterName = $report->informer->nickname;
|
||||||
->map(function ($report) {
|
}
|
||||||
$uploader = User::find($report->uploader);
|
$report->getAttribute('texture');
|
||||||
if ($uploader) {
|
|
||||||
$report->uploaderName = $uploader->nickname;
|
|
||||||
}
|
|
||||||
if ($report->informer) {
|
|
||||||
$report->reporterName = $report->informer->nickname;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $report;
|
return $report;
|
||||||
});
|
});
|
||||||
|
$pagination->setCollection($collection);
|
||||||
|
|
||||||
return [
|
return $pagination;
|
||||||
'totalRecords' => Report::count(),
|
|
||||||
'data' => $reports,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function review(Request $request, Dispatcher $dispatcher)
|
public function review(
|
||||||
{
|
Report $report,
|
||||||
$data = $this->validate($request, [
|
Request $request,
|
||||||
'id' => 'required|exists:reports',
|
Dispatcher $dispatcher
|
||||||
|
) {
|
||||||
|
$data = $request->validate([
|
||||||
'action' => ['required', Rule::in(['delete', 'ban', 'reject'])],
|
'action' => ['required', Rule::in(['delete', 'ban', 'reject'])],
|
||||||
]);
|
]);
|
||||||
$action = $data['action'];
|
$action = $data['action'];
|
||||||
$report = Report::find($data['id']);
|
|
||||||
|
|
||||||
$dispatcher->dispatch('report.reviewing', [$report, $action]);
|
$dispatcher->dispatch('report.reviewing', [$report, $action]);
|
||||||
|
|
||||||
@ -127,8 +117,10 @@ class ReportController extends Controller
|
|||||||
|
|
||||||
switch ($action) {
|
switch ($action) {
|
||||||
case 'delete':
|
case 'delete':
|
||||||
|
/** @var Texture */
|
||||||
$texture = $report->texture;
|
$texture = $report->texture;
|
||||||
if ($texture) {
|
if ($texture) {
|
||||||
|
Storage::disk('textures')->delete($texture->hash);
|
||||||
$texture->delete();
|
$texture->delete();
|
||||||
$dispatcher->dispatch('texture.deleted', [$texture]);
|
$dispatcher->dispatch('texture.deleted', [$texture]);
|
||||||
} else {
|
} else {
|
||||||
|
@ -5,6 +5,7 @@ namespace App\Models;
|
|||||||
use DateTimeInterface;
|
use DateTimeInterface;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
use Lorisleiva\LaravelSearchString\Concerns\SearchString;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property int $id
|
* @property int $id
|
||||||
@ -19,6 +20,8 @@ use Illuminate\Support\Carbon;
|
|||||||
*/
|
*/
|
||||||
class Report extends Model
|
class Report extends Model
|
||||||
{
|
{
|
||||||
|
use SearchString;
|
||||||
|
|
||||||
public const CREATED_AT = 'report_at';
|
public const CREATED_AT = 'report_at';
|
||||||
public const UPDATED_AT = null;
|
public const UPDATED_AT = null;
|
||||||
|
|
||||||
@ -33,6 +36,12 @@ class Report extends Model
|
|||||||
'status' => 'integer',
|
'status' => 'integer',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
protected $searchStringColumns = [
|
||||||
|
'id', 'tid', 'uploader', 'reporter',
|
||||||
|
'reason', 'status',
|
||||||
|
'report_at' => ['date' => true],
|
||||||
|
];
|
||||||
|
|
||||||
public function texture()
|
public function texture()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Texture::class, 'tid', 'tid');
|
return $this->belongsTo(Texture::class, 'tid', 'tid');
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Models\Concerns\HasPassword;
|
use App\Models\Concerns\HasPassword;
|
||||||
use DateTimeInterface;
|
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
@ -63,7 +63,7 @@ export default [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'admin/reports',
|
path: 'admin/reports',
|
||||||
component: () => import('../views/admin/Reports.vue'),
|
react: () => import('../views/admin/ReportsManagement'),
|
||||||
el: '.content > .container-fluid',
|
el: '.content > .container-fluid',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1,144 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="container-fluid">
|
|
||||||
<vue-good-table
|
|
||||||
mode="remote"
|
|
||||||
:rows="reports"
|
|
||||||
:total-rows="totalRecords || reports.length"
|
|
||||||
:columns="columns"
|
|
||||||
:search-options="tableOptions.search"
|
|
||||||
:pagination-options="tableOptions.pagination"
|
|
||||||
style-class="vgt-table striped"
|
|
||||||
@on-page-change="onPageChange"
|
|
||||||
@on-sort-change="onSortChange"
|
|
||||||
@on-search="onSearch"
|
|
||||||
@on-per-page-change="onPerPageChange"
|
|
||||||
>
|
|
||||||
<template #table-row="props">
|
|
||||||
<span v-if="props.column.field === 'tid'">
|
|
||||||
{{ props.formattedRow[props.column.field] }}
|
|
||||||
<a :href="`${baseUrl}/skinlib/show/${props.row.tid}`">{{ $t('report.check') }}</a>
|
|
||||||
<a href="#" @click="deleteTexture(props.row)">
|
|
||||||
{{ $t('report.delete') }}
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
<span v-else-if="props.column.field === 'uploader'">
|
|
||||||
{{ props.row.uploaderName }} (UID: {{ props.row.uploader }})
|
|
||||||
<a href="#" @click="ban(props.row)">
|
|
||||||
{{ $t('report.ban') }}
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
<span v-else-if="props.column.field === 'reporter'">
|
|
||||||
{{ props.row.reporterName }} (UID: {{ props.row.reporter }})
|
|
||||||
</span>
|
|
||||||
<span v-else-if="props.column.field === 'status'">
|
|
||||||
{{ $t(`report.status.${props.row.status}`) }}
|
|
||||||
</span>
|
|
||||||
<span v-else-if="props.column.field === 'ops'">
|
|
||||||
<button class="btn btn-default" @click="reject(props.row)">
|
|
||||||
{{ $t('report.reject') }}
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
<span v-else>
|
|
||||||
{{ props.formattedRow[props.column.field] }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</vue-good-table>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { VueGoodTable } from 'vue-good-table'
|
|
||||||
import 'vue-good-table/dist/vue-good-table.min.css'
|
|
||||||
import tableOptions from '../../components/mixins/tableOptions'
|
|
||||||
import serverTable from '../../components/mixins/serverTable'
|
|
||||||
import emitMounted from '../../components/mixins/emitMounted'
|
|
||||||
import { toast } from '../../scripts/notify'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'ReportsManagement',
|
|
||||||
components: {
|
|
||||||
VueGoodTable,
|
|
||||||
},
|
|
||||||
mixins: [
|
|
||||||
emitMounted,
|
|
||||||
tableOptions,
|
|
||||||
serverTable,
|
|
||||||
],
|
|
||||||
props: {
|
|
||||||
baseUrl: {
|
|
||||||
type: String,
|
|
||||||
default: blessing.base_url,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
reports: [],
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
field: 'id', type: 'number', hidden: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'tid', label: this.$t('report.tid'), type: 'number',
|
|
||||||
},
|
|
||||||
{ field: 'uploader', label: this.$t('skinlib.show.uploader') },
|
|
||||||
{ field: 'reporter', label: this.$t('report.reporter') },
|
|
||||||
{
|
|
||||||
field: 'reason',
|
|
||||||
label: this.$t('report.reason'),
|
|
||||||
sortable: false,
|
|
||||||
width: '23%',
|
|
||||||
},
|
|
||||||
{ field: 'status', label: this.$t('report.status-title') },
|
|
||||||
{
|
|
||||||
field: 'report_at',
|
|
||||||
label: this.$t('report.time'),
|
|
||||||
globalSearchDisabled: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'ops',
|
|
||||||
label: this.$t('admin.operationsTitle'),
|
|
||||||
globalSearchDisabled: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.sortField = 'report_at'
|
|
||||||
this.sortType = 'desc'
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async fetchData() {
|
|
||||||
const { data, totalRecords } = await this.$http.get(
|
|
||||||
'/admin/reports/list',
|
|
||||||
this.serverParams,
|
|
||||||
)
|
|
||||||
this.totalRecords = totalRecords
|
|
||||||
this.reports = data
|
|
||||||
},
|
|
||||||
deleteTexture(report) {
|
|
||||||
this.resolve(report, 'delete')
|
|
||||||
},
|
|
||||||
ban(report) {
|
|
||||||
this.resolve(report, 'ban')
|
|
||||||
},
|
|
||||||
reject(report) {
|
|
||||||
this.resolve(report, 'reject')
|
|
||||||
},
|
|
||||||
async resolve(report, action) {
|
|
||||||
const {
|
|
||||||
code, message, data,
|
|
||||||
} = await this.$http.post(
|
|
||||||
'/admin/reports',
|
|
||||||
{ id: report.id, action },
|
|
||||||
)
|
|
||||||
if (code === 0) {
|
|
||||||
toast.success(message)
|
|
||||||
report.status = data.status
|
|
||||||
} else {
|
|
||||||
toast.error(message)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -0,0 +1,31 @@
|
|||||||
|
@use '../../../styles/utils';
|
||||||
|
|
||||||
|
.box {
|
||||||
|
width: 240px;
|
||||||
|
transition-property: box-shadow;
|
||||||
|
transition-duration: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
flex: unset;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 170px;
|
||||||
|
height: 170px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 2.5px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reason {
|
||||||
|
@include utils.truncate-text;
|
||||||
|
}
|
116
resources/assets/src/views/admin/ReportsManagement/ImageBox.tsx
Normal file
116
resources/assets/src/views/admin/ReportsManagement/ImageBox.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { t } from '@/scripts/i18n'
|
||||||
|
import { Texture } from '@/scripts/types'
|
||||||
|
import { Report, Status } from './types'
|
||||||
|
import styles from './ImageBox.module.scss'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
report: Report
|
||||||
|
onClick(texture: Texture | null): void
|
||||||
|
onBan(): void
|
||||||
|
onDelete(): void
|
||||||
|
onReject(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ImageBox: React.FC<Props> = (props) => {
|
||||||
|
const { report } = props
|
||||||
|
|
||||||
|
const handleImageClick = () => props.onClick(report.texture)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`card mr-3 mb-3 ${styles.box}`}>
|
||||||
|
<div className="card-header">
|
||||||
|
<b>
|
||||||
|
{t('skinlib.show.uploader')}
|
||||||
|
{': '}
|
||||||
|
</b>
|
||||||
|
<span className="mr-1">{report.uploaderName}</span>
|
||||||
|
(UID: {report.uploader})
|
||||||
|
</div>
|
||||||
|
<div className={`card-body ${styles.body}`}>
|
||||||
|
<img
|
||||||
|
src={`${blessing.base_url}/preview/${report.tid}?height=150`}
|
||||||
|
alt={report.tid.toString()}
|
||||||
|
className="card-img-top"
|
||||||
|
onClick={handleImageClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={`card-footer ${styles.footer}`}>
|
||||||
|
<div className="d-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
{report.status === Status.Pending ? (
|
||||||
|
<span className="badge bg-warning">{t('report.status.0')}</span>
|
||||||
|
) : report.status === Status.Resolved ? (
|
||||||
|
<span className="badge bg-success">{t('report.status.1')}</span>
|
||||||
|
) : (
|
||||||
|
<span className="badge bg-danger">{t('report.status.2')}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="dropdown">
|
||||||
|
<a
|
||||||
|
className="text-gray"
|
||||||
|
href="#"
|
||||||
|
data-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<i className="fas fa-cog"></i>
|
||||||
|
</a>
|
||||||
|
<div className="dropdown-menu dropdown-menu-right">
|
||||||
|
<a
|
||||||
|
href={`${blessing.base_url}/skinlib/show/${report.tid}`}
|
||||||
|
className="dropdown-item"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<i className="fas fa-share-square mr-2"></i>
|
||||||
|
{t('user.viewInSkinlib')}
|
||||||
|
</a>
|
||||||
|
<a href="#" className="dropdown-item" onClick={props.onBan}>
|
||||||
|
<i className="fas fa-user-slash mr-2"></i>
|
||||||
|
{t('report.ban')}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="dropdown-item dropdown-item-danger"
|
||||||
|
onClick={props.onDelete}
|
||||||
|
>
|
||||||
|
<i className="fas fa-trash mr-2"></i>
|
||||||
|
{t('skinlib.show.delete-texture')}
|
||||||
|
</a>
|
||||||
|
<a href="#" className="dropdown-item" onClick={props.onReject}>
|
||||||
|
<i className="fas fa-thumbs-down mr-2"></i>
|
||||||
|
{t('report.reject')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b>
|
||||||
|
{t('report.reporter')}
|
||||||
|
{': '}
|
||||||
|
</b>
|
||||||
|
<span className="mr-1">{report.reporterName}</span>
|
||||||
|
(UID: {report.reporter})
|
||||||
|
</div>
|
||||||
|
<details>
|
||||||
|
<summary className={styles.reason}>
|
||||||
|
<b>
|
||||||
|
{t('report.reason')}
|
||||||
|
{': '}
|
||||||
|
</b>
|
||||||
|
{report.reason}
|
||||||
|
</summary>
|
||||||
|
<div>{report.reason}</div>
|
||||||
|
<div>
|
||||||
|
<small>
|
||||||
|
{t('report.time')}
|
||||||
|
{': '}
|
||||||
|
{report.report_at}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImageBox
|
155
resources/assets/src/views/admin/ReportsManagement/index.tsx
Normal file
155
resources/assets/src/views/admin/ReportsManagement/index.tsx
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import { hot } from 'react-hot-loader/root'
|
||||||
|
import { useImmer } from 'use-immer'
|
||||||
|
import { t } from '@/scripts/i18n'
|
||||||
|
import * as fetch from '@/scripts/net'
|
||||||
|
import { Paginator, Texture, TextureType } from '@/scripts/types'
|
||||||
|
import { toast, showModal } from '@/scripts/notify'
|
||||||
|
import Loading from '@/components/Loading'
|
||||||
|
import Pagination from '@/components/Pagination'
|
||||||
|
import ViewerSkeleton from '@/components/ViewerSkeleton'
|
||||||
|
import { Report, Status } from './types'
|
||||||
|
import ImageBox from './ImageBox'
|
||||||
|
|
||||||
|
const Previewer = React.lazy(() => import('@/components/Viewer'))
|
||||||
|
|
||||||
|
const ReportsManagement: React.FC = () => {
|
||||||
|
const [reports, setReports] = useImmer<Report[]>([])
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const [totalPages, setTotalPages] = useState(1)
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [query, setQuery] = useState('status:0 sort:-report_at')
|
||||||
|
const [viewingTexture, setViewingTexture] = useState<Texture | null>(null)
|
||||||
|
|
||||||
|
const getReports = async () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
const { data, last_page }: Paginator<Report> = await fetch.get(
|
||||||
|
'/admin/reports/list',
|
||||||
|
{
|
||||||
|
q: query,
|
||||||
|
page,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
setTotalPages(last_page)
|
||||||
|
setReports(() => data)
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getReports()
|
||||||
|
}, [page])
|
||||||
|
|
||||||
|
const handleQueryChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setQuery(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmitQuery = (event: React.FormEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
getReports()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleProceedReport = async (
|
||||||
|
report: Report,
|
||||||
|
index: number,
|
||||||
|
action: 'ban' | 'delete' | 'reject',
|
||||||
|
) => {
|
||||||
|
type Ok = { code: 0; message: string; data: { status: Status } }
|
||||||
|
type Err = { code: 1; message: string }
|
||||||
|
const resp = await fetch.put<Ok | Err>(`/admin/reports/${report.id}`, {
|
||||||
|
action,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (resp.code === 0) {
|
||||||
|
toast.success(resp.message)
|
||||||
|
setReports((reports) => {
|
||||||
|
reports[index].status = resp.data.status
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
toast.error(resp.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (report: Report, index: number) => {
|
||||||
|
try {
|
||||||
|
await showModal({
|
||||||
|
text: t('skinlib.deleteNotice'),
|
||||||
|
okButtonType: 'danger',
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handleProceedReport(report, index, 'delete')
|
||||||
|
}
|
||||||
|
|
||||||
|
const textureUrl =
|
||||||
|
viewingTexture && `${blessing.base_url}/textures/${viewingTexture.hash}`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-lg-8">
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header">
|
||||||
|
<form className="input-group" onSubmit={handleSubmitQuery}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
title={t('vendor.datatable.search')}
|
||||||
|
value={query}
|
||||||
|
onChange={handleQueryChange}
|
||||||
|
/>
|
||||||
|
<div className="input-group-append">
|
||||||
|
<button className="btn btn-primary" type="submit">
|
||||||
|
{t('vendor.datatable.search')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="card-body">
|
||||||
|
<Loading />
|
||||||
|
</div>
|
||||||
|
) : reports.length === 0 ? (
|
||||||
|
<div className="card-body text-center">{t('general.noResult')}</div>
|
||||||
|
) : (
|
||||||
|
<div className="card-body d-flex flex-wrap">
|
||||||
|
{reports.map((report, i) => (
|
||||||
|
<ImageBox
|
||||||
|
key={report.id}
|
||||||
|
report={report}
|
||||||
|
onClick={setViewingTexture}
|
||||||
|
onBan={() => handleProceedReport(report, i, 'ban')}
|
||||||
|
onDelete={() => handleDelete(report, i)}
|
||||||
|
onReject={() => handleProceedReport(report, i, 'reject')}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="card-footer">
|
||||||
|
<div className="float-right">
|
||||||
|
<Pagination
|
||||||
|
page={page}
|
||||||
|
totalPages={totalPages}
|
||||||
|
onChange={setPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-lg-4">
|
||||||
|
<React.Suspense fallback={<ViewerSkeleton />}>
|
||||||
|
<Previewer
|
||||||
|
{...{
|
||||||
|
[viewingTexture?.type === TextureType.Cape
|
||||||
|
? TextureType.Cape
|
||||||
|
: 'skin']: textureUrl,
|
||||||
|
}}
|
||||||
|
isAlex={viewingTexture?.type === TextureType.Alex}
|
||||||
|
/>
|
||||||
|
</React.Suspense>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default hot(ReportsManagement)
|
20
resources/assets/src/views/admin/ReportsManagement/types.ts
Normal file
20
resources/assets/src/views/admin/ReportsManagement/types.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Texture } from '@/scripts/types'
|
||||||
|
|
||||||
|
export const enum Status {
|
||||||
|
Pending = 0,
|
||||||
|
Resolved = 1,
|
||||||
|
Rejected = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Report = {
|
||||||
|
id: number
|
||||||
|
tid: number
|
||||||
|
texture: Texture | null
|
||||||
|
uploader: number
|
||||||
|
uploaderName: string
|
||||||
|
reporter: number
|
||||||
|
reporterName: string
|
||||||
|
reason: string
|
||||||
|
status: Status
|
||||||
|
report_at: string
|
||||||
|
}
|
@ -1,102 +0,0 @@
|
|||||||
import Vue from 'vue'
|
|
||||||
import { mount } from '@vue/test-utils'
|
|
||||||
import { flushPromises } from '../../utils'
|
|
||||||
import { toast } from '@/scripts/notify'
|
|
||||||
import Reports from '@/views/admin/Reports.vue'
|
|
||||||
|
|
||||||
jest.mock('@/scripts/notify')
|
|
||||||
|
|
||||||
test('basic render', async () => {
|
|
||||||
Vue.prototype.$http.get.mockResolvedValue({
|
|
||||||
data: [{
|
|
||||||
id: 1,
|
|
||||||
uploader: 1,
|
|
||||||
uploaderName: 'a',
|
|
||||||
reporter: 2,
|
|
||||||
reporterName: 'b',
|
|
||||||
reason: 'sth',
|
|
||||||
status: 0,
|
|
||||||
}],
|
|
||||||
})
|
|
||||||
const wrapper = mount(Reports)
|
|
||||||
await flushPromises()
|
|
||||||
const text = wrapper.text()
|
|
||||||
expect(text).toContain('a (UID: 1)')
|
|
||||||
expect(text).toContain('b (UID: 2)')
|
|
||||||
expect(text).toContain('sth')
|
|
||||||
expect(text).toContain('report.status.0')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('link to skin library', async () => {
|
|
||||||
Vue.prototype.$http.get.mockResolvedValue({
|
|
||||||
data: [{ id: 1, tid: 1 }],
|
|
||||||
})
|
|
||||||
const wrapper = mount(Reports)
|
|
||||||
await flushPromises()
|
|
||||||
expect(wrapper.find('a').attributes('href')).toBe('/skinlib/show/1')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('delete texture', async () => {
|
|
||||||
Vue.prototype.$http.get.mockResolvedValue({ data: [{ id: 1, status: 0 }] })
|
|
||||||
Vue.prototype.$http.post
|
|
||||||
.mockResolvedValueOnce({ code: 1, message: 'fail' })
|
|
||||||
.mockResolvedValue({
|
|
||||||
code: 0, message: 'ok', data: { status: 1 },
|
|
||||||
})
|
|
||||||
const wrapper = mount(Reports)
|
|
||||||
await flushPromises()
|
|
||||||
const button = wrapper.findAll('a').at(1)
|
|
||||||
|
|
||||||
button.trigger('click')
|
|
||||||
await flushPromises()
|
|
||||||
expect(Vue.prototype.$http.post).toBeCalledWith(
|
|
||||||
'/admin/reports',
|
|
||||||
{ id: 1, action: 'delete' },
|
|
||||||
)
|
|
||||||
expect(toast.error).toBeCalledWith('fail')
|
|
||||||
|
|
||||||
button.trigger('click')
|
|
||||||
await flushPromises()
|
|
||||||
expect(toast.success).toBeCalledWith('ok')
|
|
||||||
expect(wrapper.text()).toContain('report.status.1')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('ban uploader', async () => {
|
|
||||||
Vue.prototype.$http.get.mockResolvedValue({ data: [{ id: 1, status: 0 }] })
|
|
||||||
Vue.prototype.$http.post
|
|
||||||
.mockResolvedValue({
|
|
||||||
code: 0, message: 'ok', data: { status: 1 },
|
|
||||||
})
|
|
||||||
const wrapper = mount(Reports)
|
|
||||||
await flushPromises()
|
|
||||||
const button = wrapper.findAll('a').at(2)
|
|
||||||
|
|
||||||
button.trigger('click')
|
|
||||||
await flushPromises()
|
|
||||||
expect(Vue.prototype.$http.post).toBeCalledWith(
|
|
||||||
'/admin/reports',
|
|
||||||
{ id: 1, action: 'ban' },
|
|
||||||
)
|
|
||||||
expect(toast.success).toBeCalledWith('ok')
|
|
||||||
expect(wrapper.text()).toContain('report.status.1')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('reject', async () => {
|
|
||||||
Vue.prototype.$http.get.mockResolvedValue({ data: [{ id: 1, status: 0 }] })
|
|
||||||
Vue.prototype.$http.post
|
|
||||||
.mockResolvedValue({
|
|
||||||
code: 0, message: 'ok', data: { status: 2 },
|
|
||||||
})
|
|
||||||
const wrapper = mount(Reports)
|
|
||||||
await flushPromises()
|
|
||||||
const button = wrapper.find('button')
|
|
||||||
|
|
||||||
button.trigger('click')
|
|
||||||
await flushPromises()
|
|
||||||
expect(Vue.prototype.$http.post).toBeCalledWith(
|
|
||||||
'/admin/reports',
|
|
||||||
{ id: 1, action: 'reject' },
|
|
||||||
)
|
|
||||||
expect(toast.success).toBeCalledWith('ok')
|
|
||||||
expect(wrapper.text()).toContain('report.status.2')
|
|
||||||
})
|
|
172
resources/assets/tests/views/admin/ReportsManagement.test.tsx
Normal file
172
resources/assets/tests/views/admin/ReportsManagement.test.tsx
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render, waitFor, fireEvent } from '@testing-library/react'
|
||||||
|
import { createPaginator } from '../../utils'
|
||||||
|
import { t } from '@/scripts/i18n'
|
||||||
|
import * as fetch from '@/scripts/net'
|
||||||
|
import { Texture, TextureType } from '@/scripts/types'
|
||||||
|
import ReportsManagement from '@/views/admin/ReportsManagement'
|
||||||
|
import { Report, Status } from '@/views/admin/ReportsManagement/types'
|
||||||
|
|
||||||
|
jest.mock('@/scripts/net')
|
||||||
|
|
||||||
|
const fixture: Readonly<Report> = Object.freeze<Report>({
|
||||||
|
id: 1,
|
||||||
|
tid: 1,
|
||||||
|
texture: null,
|
||||||
|
uploader: 1,
|
||||||
|
uploaderName: 'xx',
|
||||||
|
reporter: 2,
|
||||||
|
reporterName: 'yy',
|
||||||
|
reason: 'nsfw',
|
||||||
|
status: Status.Pending,
|
||||||
|
report_at: new Date().toString(),
|
||||||
|
})
|
||||||
|
|
||||||
|
test('search reports', async () => {
|
||||||
|
fetch.get.mockResolvedValue(createPaginator([]))
|
||||||
|
|
||||||
|
const { getByTitle, getByText } = render(<ReportsManagement />)
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(fetch.get).toBeCalledWith('/admin/reports/list', {
|
||||||
|
q: 'status:0 sort:-report_at',
|
||||||
|
page: 1,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
fireEvent.input(getByTitle(t('vendor.datatable.search')), {
|
||||||
|
target: { value: 's' },
|
||||||
|
})
|
||||||
|
fireEvent.click(getByText(t('vendor.datatable.search')))
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(fetch.get).toBeCalledWith('/admin/reports/list', {
|
||||||
|
q: 's',
|
||||||
|
page: 1,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('preview texture', async () => {
|
||||||
|
const texture: Texture = {
|
||||||
|
tid: fixture.tid,
|
||||||
|
name: 'cape',
|
||||||
|
type: TextureType.Cape,
|
||||||
|
hash: 'def',
|
||||||
|
size: 2,
|
||||||
|
uploader: fixture.uploader,
|
||||||
|
public: true,
|
||||||
|
upload_at: new Date().toString(),
|
||||||
|
likes: 1,
|
||||||
|
}
|
||||||
|
fetch.get.mockResolvedValue(createPaginator([{ ...fixture, texture }]))
|
||||||
|
|
||||||
|
const { getByAltText } = render(<ReportsManagement />)
|
||||||
|
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||||
|
|
||||||
|
fireEvent.click(getByAltText(fixture.tid.toString()))
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('proceed report', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
fetch.get.mockResolvedValue(createPaginator([fixture]))
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('ban uploader', () => {
|
||||||
|
it('succeeded', async () => {
|
||||||
|
fetch.put.mockResolvedValue({
|
||||||
|
code: 0,
|
||||||
|
message: 'ok',
|
||||||
|
data: { status: Status.Resolved },
|
||||||
|
})
|
||||||
|
|
||||||
|
const { getByText, getByRole, queryByText } = render(
|
||||||
|
<ReportsManagement />,
|
||||||
|
)
|
||||||
|
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||||
|
|
||||||
|
fireEvent.click(getByText(t('report.ban')))
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(fetch.put).toBeCalledWith(`/admin/reports/${fixture.id}`, {
|
||||||
|
action: 'ban',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
expect(queryByText('ok')).toBeInTheDocument()
|
||||||
|
expect(getByRole('status')).toBeInTheDocument()
|
||||||
|
expect(queryByText(t('report.status.1'))).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('failed', async () => {
|
||||||
|
fetch.put.mockResolvedValue({ code: 1, message: 'failed' })
|
||||||
|
|
||||||
|
const { getByText, getByRole, queryByText } = render(
|
||||||
|
<ReportsManagement />,
|
||||||
|
)
|
||||||
|
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||||
|
|
||||||
|
fireEvent.click(getByText(t('report.ban')))
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(fetch.put).toBeCalledWith(`/admin/reports/${fixture.id}`, {
|
||||||
|
action: 'ban',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
expect(queryByText('failed')).toBeInTheDocument()
|
||||||
|
expect(getByRole('alert')).toBeInTheDocument()
|
||||||
|
expect(queryByText(t('report.status.0'))).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('delete texture', () => {
|
||||||
|
it('cancelled', async () => {
|
||||||
|
const { getByText } = render(<ReportsManagement />)
|
||||||
|
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||||
|
|
||||||
|
fireEvent.click(getByText(t('skinlib.show.delete-texture')))
|
||||||
|
fireEvent.click(getByText(t('general.cancel')))
|
||||||
|
expect(fetch.put).not.toBeCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('succeeded', async () => {
|
||||||
|
fetch.put.mockResolvedValue({
|
||||||
|
code: 0,
|
||||||
|
message: 'ok',
|
||||||
|
data: { status: Status.Resolved },
|
||||||
|
})
|
||||||
|
|
||||||
|
const { getByText, getByRole, queryByText } = render(
|
||||||
|
<ReportsManagement />,
|
||||||
|
)
|
||||||
|
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||||
|
|
||||||
|
fireEvent.click(getByText(t('skinlib.show.delete-texture')))
|
||||||
|
fireEvent.click(getByText(t('general.confirm')))
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(fetch.put).toBeCalledWith(`/admin/reports/${fixture.id}`, {
|
||||||
|
action: 'delete',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
expect(queryByText('ok')).toBeInTheDocument()
|
||||||
|
expect(getByRole('status')).toBeInTheDocument()
|
||||||
|
expect(queryByText(t('report.status.1'))).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('reject report', async () => {
|
||||||
|
fetch.put.mockResolvedValue({
|
||||||
|
code: 0,
|
||||||
|
message: 'ok',
|
||||||
|
data: { status: Status.Rejected },
|
||||||
|
})
|
||||||
|
|
||||||
|
const { getByText, getByRole, queryByText } = render(<ReportsManagement />)
|
||||||
|
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||||
|
|
||||||
|
fireEvent.click(getByText(t('report.reject')))
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(fetch.put).toBeCalledWith(`/admin/reports/${fixture.id}`, {
|
||||||
|
action: 'reject',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
expect(queryByText('ok')).toBeInTheDocument()
|
||||||
|
expect(getByRole('status')).toBeInTheDocument()
|
||||||
|
expect(queryByText(t('report.status.2'))).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
@ -73,6 +73,7 @@
|
|||||||
- Fixed potential "Invalid Signature" issue.
|
- Fixed potential "Invalid Signature" issue.
|
||||||
- Fixed that duplicated player name is not detected when updating player name in administration panel.
|
- Fixed that duplicated player name is not detected when updating player name in administration panel.
|
||||||
- Fixed that normal administrator can set other user as administrator.
|
- Fixed that normal administrator can set other user as administrator.
|
||||||
|
- Fixed that texture file won't be deleted when deleting texture in reports management.
|
||||||
|
|
||||||
## Removed
|
## Removed
|
||||||
|
|
||||||
|
@ -73,6 +73,7 @@
|
|||||||
- 修复可能的「Invalid Signature」问题
|
- 修复可能的「Invalid Signature」问题
|
||||||
- 修复在管理面板中修改角色名时不检测角色名是否重复的问题
|
- 修复在管理面板中修改角色名时不检测角色名是否重复的问题
|
||||||
- 修复普通管理员可设置其他用户为管理员的问题
|
- 修复普通管理员可设置其他用户为管理员的问题
|
||||||
|
- 修复处理举报中删除材质时不删除材质文件的问题
|
||||||
|
|
||||||
## 移除
|
## 移除
|
||||||
|
|
||||||
|
@ -61,5 +61,10 @@ Route::prefix('admin')
|
|||||||
Route::delete('{uid}', 'ClosetManagementController@remove');
|
Route::delete('{uid}', 'ClosetManagementController@remove');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Route::prefix('reports')->group(function () {
|
||||||
|
Route::get('', 'ReportController@manage');
|
||||||
|
Route::put('{report}', 'ReportController@review');
|
||||||
|
});
|
||||||
|
|
||||||
Route::post('notifications', 'NotificationsController@send');
|
Route::post('notifications', 'NotificationsController@send');
|
||||||
});
|
});
|
||||||
|
@ -159,8 +159,8 @@ Route::prefix('admin')
|
|||||||
|
|
||||||
Route::prefix('reports')->group(function () {
|
Route::prefix('reports')->group(function () {
|
||||||
Route::view('', 'admin.reports');
|
Route::view('', 'admin.reports');
|
||||||
Route::post('', 'ReportController@review');
|
Route::put('{report}', 'ReportController@review');
|
||||||
Route::any('list', 'ReportController@manage');
|
Route::get('list', 'ReportController@manage');
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::prefix('i18n')->group(function () {
|
Route::prefix('i18n')->group(function () {
|
||||||
|
@ -9,6 +9,7 @@ use Blessing\Filter;
|
|||||||
use Blessing\Rejection;
|
use Blessing\Rejection;
|
||||||
use Event;
|
use Event;
|
||||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
class ReportControllerTest extends TestCase
|
class ReportControllerTest extends TestCase
|
||||||
{
|
{
|
||||||
@ -22,20 +23,20 @@ class ReportControllerTest extends TestCase
|
|||||||
$user = factory(User::class)->create();
|
$user = factory(User::class)->create();
|
||||||
$texture = factory(Texture::class)->create();
|
$texture = factory(Texture::class)->create();
|
||||||
|
|
||||||
// Without `tid` field
|
// without `tid` field
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->postJson('/skinlib/report')
|
->postJson('/skinlib/report')
|
||||||
->assertJsonValidationErrors('tid');
|
->assertJsonValidationErrors('tid');
|
||||||
|
|
||||||
// Invalid texture
|
// invalid texture
|
||||||
$this->postJson('/skinlib/report', ['tid' => $texture->tid - 1])
|
$this->postJson('/skinlib/report', ['tid' => $texture->tid - 1])
|
||||||
->assertJsonValidationErrors('tid');
|
->assertJsonValidationErrors('tid');
|
||||||
|
|
||||||
// Without `reason` field
|
// without `reason` field
|
||||||
$this->postJson('/skinlib/report', ['tid' => $texture->tid])
|
$this->postJson('/skinlib/report', ['tid' => $texture->tid])
|
||||||
->assertJsonValidationErrors('reason');
|
->assertJsonValidationErrors('reason');
|
||||||
|
|
||||||
// Lack of score
|
// lack of score
|
||||||
$user->score = 0;
|
$user->score = 0;
|
||||||
$user->save();
|
$user->save();
|
||||||
option(['reporter_score_modification' => -5]);
|
option(['reporter_score_modification' => -5]);
|
||||||
@ -46,7 +47,7 @@ class ReportControllerTest extends TestCase
|
|||||||
]);
|
]);
|
||||||
option(['reporter_score_modification' => 5]);
|
option(['reporter_score_modification' => 5]);
|
||||||
|
|
||||||
// Rejection
|
// rejection
|
||||||
$filter->add(
|
$filter->add(
|
||||||
'user_can_report',
|
'user_can_report',
|
||||||
function ($can, $tid, $reason, $reporter) use ($texture, $user) {
|
function ($can, $tid, $reason, $reporter) use ($texture, $user) {
|
||||||
@ -61,7 +62,7 @@ class ReportControllerTest extends TestCase
|
|||||||
->assertJson(['code' => 1, 'message' => 'rejected']);
|
->assertJson(['code' => 1, 'message' => 'rejected']);
|
||||||
$filter->remove('user_can_report');
|
$filter->remove('user_can_report');
|
||||||
|
|
||||||
// Success
|
// success
|
||||||
$this->postJson('/skinlib/report', ['tid' => $texture->tid, 'reason' => 'reason'])
|
$this->postJson('/skinlib/report', ['tid' => $texture->tid, 'reason' => 'reason'])
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'code' => 0,
|
'code' => 0,
|
||||||
@ -93,7 +94,7 @@ class ReportControllerTest extends TestCase
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Prevent duplication
|
// prevent duplication
|
||||||
$this->postJson('/skinlib/report', ['tid' => $texture->tid, 'reason' => 'reason'])
|
$this->postJson('/skinlib/report', ['tid' => $texture->tid, 'reason' => 'reason'])
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'code' => 1,
|
'code' => 1,
|
||||||
@ -134,18 +135,7 @@ class ReportControllerTest extends TestCase
|
|||||||
|
|
||||||
$this->actingAs($reporter)
|
$this->actingAs($reporter)
|
||||||
->getJson('/admin/reports/list')
|
->getJson('/admin/reports/list')
|
||||||
->assertJson([
|
->assertJson(['data' => [$report->toArray()]]);
|
||||||
'totalRecords' => 1,
|
|
||||||
'data' => [[
|
|
||||||
'tid' => $texture->tid,
|
|
||||||
'uploader' => $uploader->uid,
|
|
||||||
'reporter' => $reporter->uid,
|
|
||||||
'reason' => 'test',
|
|
||||||
'status' => Report::PENDING,
|
|
||||||
'uploaderName' => $uploader->nickname,
|
|
||||||
'reporterName' => $reporter->nickname,
|
|
||||||
]],
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testReview()
|
public function testReview()
|
||||||
@ -164,25 +154,17 @@ class ReportControllerTest extends TestCase
|
|||||||
$report->save();
|
$report->save();
|
||||||
$report->refresh();
|
$report->refresh();
|
||||||
|
|
||||||
// Without `id` field
|
|
||||||
$this->actingAs($admin)
|
|
||||||
->postJson('/admin/reports')
|
|
||||||
->assertJsonValidationErrors('id');
|
|
||||||
|
|
||||||
// Not existed
|
|
||||||
$this->postJson('/admin/reports', ['id' => $report->id - 1])
|
|
||||||
->assertJsonValidationErrors('id');
|
|
||||||
|
|
||||||
// Without `action` field
|
// Without `action` field
|
||||||
$this->postJson('/admin/reports', ['id' => $report->id])
|
$this->actingAs($admin)
|
||||||
|
->putJson('/admin/reports/'.$report->id)
|
||||||
->assertJsonValidationErrors('action');
|
->assertJsonValidationErrors('action');
|
||||||
|
|
||||||
// Invalid action
|
// invalid action
|
||||||
$this->postJson('/admin/reports', ['id' => $report->id, 'action' => 'a'])
|
$this->putJson('/admin/reports/'.$report->id, ['action' => 'a'])
|
||||||
->assertJsonValidationErrors('action');
|
->assertJsonValidationErrors('action');
|
||||||
|
|
||||||
// Allow to process again
|
// allow to process again
|
||||||
$this->postJson('/admin/reports', ['id' => $report->id, 'action' => 'reject'])
|
$this->putJson('/admin/reports/'.$report->id, ['action' => 'reject'])
|
||||||
->assertJson(['code' => 0]);
|
->assertJson(['code' => 0]);
|
||||||
$id = $report->id;
|
$id = $report->id;
|
||||||
Event::assertDispatched('report.reviewing', function ($event, $payload) use ($id) {
|
Event::assertDispatched('report.reviewing', function ($event, $payload) use ($id) {
|
||||||
@ -213,10 +195,10 @@ class ReportControllerTest extends TestCase
|
|||||||
$report->refresh();
|
$report->refresh();
|
||||||
$id = $report->id;
|
$id = $report->id;
|
||||||
|
|
||||||
// Should not cost score
|
// should not cost score
|
||||||
$score = $reporter->score;
|
$score = $reporter->score;
|
||||||
$this->actingAs($admin)
|
$this->actingAs($admin)
|
||||||
->postJson('/admin/reports', ['id' => $report->id, 'action' => 'reject'])
|
->putJson('/admin/reports/'.$report->id, ['action' => 'reject'])
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'code' => 0,
|
'code' => 0,
|
||||||
'message' => trans('general.op-success'),
|
'message' => trans('general.op-success'),
|
||||||
@ -240,12 +222,12 @@ class ReportControllerTest extends TestCase
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Should cost score
|
// should cost score
|
||||||
$report->status = Report::PENDING;
|
$report->status = Report::PENDING;
|
||||||
$report->save();
|
$report->save();
|
||||||
option(['reporter_score_modification' => 5]);
|
option(['reporter_score_modification' => 5]);
|
||||||
$score = $reporter->score;
|
$score = $reporter->score;
|
||||||
$this->postJson('/admin/reports', ['id' => $report->id, 'action' => 'reject'])
|
$this->putJson('/admin/reports/'.$report->id, ['action' => 'reject'])
|
||||||
->assertJson(['code' => 0]);
|
->assertJson(['code' => 0]);
|
||||||
$reporter->refresh();
|
$reporter->refresh();
|
||||||
$this->assertEquals($score - 5, $reporter->score);
|
$this->assertEquals($score - 5, $reporter->score);
|
||||||
@ -254,11 +236,13 @@ class ReportControllerTest extends TestCase
|
|||||||
public function testReviewDelete()
|
public function testReviewDelete()
|
||||||
{
|
{
|
||||||
Event::fake();
|
Event::fake();
|
||||||
|
$disk = Storage::fake('textures');
|
||||||
|
|
||||||
$uploader = factory(User::class)->create();
|
$uploader = factory(User::class)->create();
|
||||||
$reporter = factory(User::class)->create();
|
$reporter = factory(User::class)->create();
|
||||||
$admin = factory(User::class)->states('admin')->create();
|
$admin = factory(User::class)->states('admin')->create();
|
||||||
$texture = factory(Texture::class)->create(['uploader' => $uploader->uid]);
|
$texture = factory(Texture::class)->create(['uploader' => $uploader->uid]);
|
||||||
|
$disk->put($texture->hash, '');
|
||||||
|
|
||||||
$report = new Report();
|
$report = new Report();
|
||||||
$report->tid = $texture->tid;
|
$report->tid = $texture->tid;
|
||||||
@ -278,7 +262,7 @@ class ReportControllerTest extends TestCase
|
|||||||
]);
|
]);
|
||||||
$score = $reporter->score;
|
$score = $reporter->score;
|
||||||
$this->actingAs($admin)
|
$this->actingAs($admin)
|
||||||
->postJson('/admin/reports', ['id' => $report->id, 'action' => 'delete'])
|
->putJson('/admin/reports/'.$report->id, ['action' => 'delete'])
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'code' => 0,
|
'code' => 0,
|
||||||
'message' => trans('general.op-success'),
|
'message' => trans('general.op-success'),
|
||||||
@ -289,6 +273,7 @@ class ReportControllerTest extends TestCase
|
|||||||
$this->assertEquals(Report::RESOLVED, $report->status);
|
$this->assertEquals(Report::RESOLVED, $report->status);
|
||||||
$this->assertNull(Texture::find($texture->tid));
|
$this->assertNull(Texture::find($texture->tid));
|
||||||
$this->assertEquals($score + 7, $reporter->score);
|
$this->assertEquals($score + 7, $reporter->score);
|
||||||
|
Storage::assertMissing($texture->hash);
|
||||||
Event::assertDispatched('report.reviewing', function ($event, $payload) use ($id) {
|
Event::assertDispatched('report.reviewing', function ($event, $payload) use ($id) {
|
||||||
[$report, $action] = $payload;
|
[$report, $action] = $payload;
|
||||||
$this->assertEquals($id, $report->id);
|
$this->assertEquals($id, $report->id);
|
||||||
@ -337,7 +322,7 @@ class ReportControllerTest extends TestCase
|
|||||||
$score = $reporter->score;
|
$score = $reporter->score;
|
||||||
$texture->delete();
|
$texture->delete();
|
||||||
$this->actingAs($admin)
|
$this->actingAs($admin)
|
||||||
->postJson('/admin/reports', ['id' => $report->id, 'action' => 'delete'])
|
->putJson('/admin/reports/'.$report->id, ['action' => 'delete'])
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'code' => 0,
|
'code' => 0,
|
||||||
'message' => trans('general.texture-deleted'),
|
'message' => trans('general.texture-deleted'),
|
||||||
@ -375,11 +360,11 @@ class ReportControllerTest extends TestCase
|
|||||||
$report->refresh();
|
$report->refresh();
|
||||||
$id = $report->id;
|
$id = $report->id;
|
||||||
|
|
||||||
// Uploader should be banned
|
// uploader should be banned
|
||||||
option(['reporter_reward_score' => 6]);
|
option(['reporter_reward_score' => 6]);
|
||||||
$score = $reporter->score;
|
$score = $reporter->score;
|
||||||
$this->actingAs($admin)
|
$this->actingAs($admin)
|
||||||
->postJson('/admin/reports', ['id' => $report->id, 'action' => 'ban'])
|
->putJson('/admin/reports/'.$report->id, ['action' => 'ban'])
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'code' => 0,
|
'code' => 0,
|
||||||
'message' => trans('general.op-success'),
|
'message' => trans('general.op-success'),
|
||||||
@ -391,14 +376,14 @@ class ReportControllerTest extends TestCase
|
|||||||
$this->assertEquals($score + 6, $reporter->score);
|
$this->assertEquals($score + 6, $reporter->score);
|
||||||
option(['reporter_reward_score' => 0]);
|
option(['reporter_reward_score' => 0]);
|
||||||
|
|
||||||
// Should not ban admin uploader
|
// should not ban admin uploader
|
||||||
$report->refresh();
|
$report->refresh();
|
||||||
$report->status = Report::PENDING;
|
$report->status = Report::PENDING;
|
||||||
$report->save();
|
$report->save();
|
||||||
$uploader->refresh();
|
$uploader->refresh();
|
||||||
$uploader->permission = User::ADMIN;
|
$uploader->permission = User::ADMIN;
|
||||||
$uploader->save();
|
$uploader->save();
|
||||||
$this->postJson('/admin/reports', ['id' => $report->id, 'action' => 'ban'])
|
$this->putJson('/admin/reports/'.$report->id, ['action' => 'ban'])
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'code' => 1,
|
'code' => 1,
|
||||||
'message' => trans('admin.users.operations.no-permission'),
|
'message' => trans('admin.users.operations.no-permission'),
|
||||||
@ -427,10 +412,10 @@ class ReportControllerTest extends TestCase
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Uploader has deleted its account
|
// uploader has deleted its account
|
||||||
$report->uploader = -1;
|
$report->uploader = -1;
|
||||||
$report->save();
|
$report->save();
|
||||||
$this->postJson('/admin/reports', ['id' => $report->id, 'action' => 'ban'])
|
$this->putJson('/admin/reports/'.$report->id, ['action' => 'ban'])
|
||||||
->assertJson([
|
->assertJson([
|
||||||
'code' => 1,
|
'code' => 1,
|
||||||
'message' => trans('admin.users.operations.non-existent'),
|
'message' => trans('admin.users.operations.non-existent'),
|
||||||
|
Loading…
Reference in New Issue
Block a user