Build plugin "report-textures" into core
This commit is contained in:
parent
d8d9923224
commit
9a095732fa
|
|
@ -128,6 +128,12 @@ class AdminController extends Controller
|
|||
$form->text('user_initial_score');
|
||||
})->handle();
|
||||
|
||||
$report = Option::form('report', OptionForm::AUTO_DETECT, function ($form) {
|
||||
$form->text('reporter_score_modification')->description();
|
||||
|
||||
$form->text('reporter_reward_score');
|
||||
})->handle();
|
||||
|
||||
$sign = Option::form('sign', OptionForm::AUTO_DETECT, function ($form) {
|
||||
$form->group('sign_score')
|
||||
->text('sign_score_from')->addon(trans('options.sign.sign_score.addon1'))
|
||||
|
|
@ -154,7 +160,7 @@ class AdminController extends Controller
|
|||
->addon(trans('general.user.score'));
|
||||
})->handle();
|
||||
|
||||
return view('admin.score', ['forms' => compact('rate', 'sign', 'sharing')]);
|
||||
return view('admin.score', ['forms' => compact('rate', 'report', 'sign', 'sharing')]);
|
||||
}
|
||||
|
||||
public function options()
|
||||
|
|
@ -215,10 +221,13 @@ class AdminController extends Controller
|
|||
|
||||
$form->text('texture_name_regexp')->hint()->placeholder();
|
||||
|
||||
$form->textarea('content_policy')->rows(3)->description();
|
||||
|
||||
$form->textarea('comment_script')->rows(6)->description();
|
||||
})->handle(function () {
|
||||
Option::set('site_name_'.config('app.locale'), request('site_name'));
|
||||
Option::set('site_description_'.config('app.locale'), request('site_description'));
|
||||
Option::set('content_policy_'.config('app.locale'), request('content_policy'));
|
||||
});
|
||||
|
||||
$announ = Option::form('announ', OptionForm::AUTO_DETECT, function ($form) {
|
||||
|
|
|
|||
137
app/Http/Controllers/ReportController.php
Normal file
137
app/Http/Controllers/ReportController.php
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Report;
|
||||
use App\Models\Texture;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class ReportController extends Controller
|
||||
{
|
||||
public function submit(Request $request)
|
||||
{
|
||||
$data = $this->validate($request, [
|
||||
'tid' => 'required|exists:textures',
|
||||
'reason' => 'required'
|
||||
]);
|
||||
$reporter = auth()->user();
|
||||
|
||||
if (Report::where('reporter', $reporter->uid)->where('tid', $data['tid'])->count() > 0) {
|
||||
return json(trans('skinlib.report.duplicate'), 1);
|
||||
}
|
||||
|
||||
$score = option('reporter_score_modification', 0);
|
||||
if ($score < 0 && $reporter->score < -$score) {
|
||||
return json(trans('skinlib.upload.lack-score'), 1);
|
||||
}
|
||||
$reporter->score += $score;
|
||||
$reporter->save();
|
||||
|
||||
$report = new Report;
|
||||
$report->tid = $data['tid'];
|
||||
$report->uploader = Texture::find($data['tid'])->uploader;
|
||||
$report->reporter = $reporter->uid;
|
||||
$report->reason = $data['reason'];
|
||||
$report->status = Report::PENDING;
|
||||
$report->save();
|
||||
|
||||
return json(trans('skinlib.report.success'), 0);
|
||||
}
|
||||
|
||||
public function viewTrack()
|
||||
{
|
||||
return view('user.report', ['user' => auth()->user()]);
|
||||
}
|
||||
|
||||
public function track()
|
||||
{
|
||||
return Report::where('reporter', auth()->id())
|
||||
->orderBy('report_at', 'desc')
|
||||
->get();
|
||||
}
|
||||
|
||||
public function manage(Request $request)
|
||||
{
|
||||
$search = $request->input('search', '');
|
||||
$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.'%')
|
||||
->orWhere('reporter', 'like', '%'.$search.'%')
|
||||
->orWhere('reason', 'like', '%'.$search.'%')
|
||||
->orderBy($sortField, $sortType)
|
||||
->offset(($page - 1) * $perPage)
|
||||
->limit($perPage)
|
||||
->get()
|
||||
->makeHidden(['informer'])
|
||||
->map(function ($report) {
|
||||
$uploader = User::find($report->uploader);
|
||||
if ($uploader) {
|
||||
$report->uploaderName = $uploader->nickname;
|
||||
}
|
||||
if ($report->informer) {
|
||||
$report->reporterName = $report->informer->nickname;
|
||||
}
|
||||
return $report;
|
||||
});
|
||||
|
||||
return [
|
||||
'totalRecords' => Report::count(),
|
||||
'data' => $reports,
|
||||
];
|
||||
}
|
||||
|
||||
public function review(Request $request)
|
||||
{
|
||||
$data = $this->validate($request, [
|
||||
'id' => 'required|exists:reports',
|
||||
'action' => ['required', Rule::in(['delete', 'ban', 'reject'])]
|
||||
]);
|
||||
$report = Report::find($data['id']);
|
||||
|
||||
if ($report->status != Report::PENDING) {
|
||||
return json(trans('admin.report-reviewed'), 1);
|
||||
}
|
||||
|
||||
if ($data['action'] == 'reject') {
|
||||
if (
|
||||
$report->informer &&
|
||||
($score = option('reporter_score_modification', 0)) > 0
|
||||
) {
|
||||
$report->informer->score -= $score;
|
||||
$report->informer->save();
|
||||
}
|
||||
$report->status = Report::REJECTED;
|
||||
$report->save();
|
||||
return json(trans('general.op-success'), 0, ['status' => Report::REJECTED]);
|
||||
}
|
||||
|
||||
switch ($data['action']) {
|
||||
case 'delete':
|
||||
$report->texture->delete();
|
||||
break;
|
||||
case 'ban':
|
||||
if (auth()->user()->permission <= $report->informer->permission) {
|
||||
return json(trans('admin.users.operations.no-permission'), 1);
|
||||
}
|
||||
$report->informer->permission = User::BANNED;
|
||||
$report->informer->save();
|
||||
break;
|
||||
}
|
||||
|
||||
$report->status = Report::RESOLVED;
|
||||
$report->save();
|
||||
|
||||
if (($score = option('reporter_score_modification', 0)) < 0) {
|
||||
$report->informer->score -= $score;
|
||||
}
|
||||
$report->informer->score += option('reporter_reward_score', 0);
|
||||
$report->informer->save();
|
||||
|
||||
return json(trans('general.op-success'), 0, ['status' => Report::RESOLVED]);
|
||||
}
|
||||
}
|
||||
|
|
@ -250,7 +250,14 @@ class SetupController extends Controller
|
|||
public static function checkTablesExist($tables = [], $returnExistingTables = false)
|
||||
{
|
||||
$existingTables = [];
|
||||
$tables = $tables ?: ['users', 'user_closet', 'players', 'textures', 'options'];
|
||||
$tables = $tables ?: [
|
||||
'users',
|
||||
'user_closet',
|
||||
'players',
|
||||
'textures',
|
||||
'options',
|
||||
'reports',
|
||||
];
|
||||
|
||||
foreach ($tables as $tableName) {
|
||||
if (Schema::hasTable($tableName)) {
|
||||
|
|
|
|||
|
|
@ -157,7 +157,8 @@ class SkinlibController extends Controller
|
|||
'currentUid' => $user ? $user->uid : 0,
|
||||
'admin' => $user && $user->isAdmin(),
|
||||
'inCloset' => $user && $user->closet()->where('tid', $texture->tid)->count() > 0,
|
||||
'nickname' => ($up = User::find($texture->uploader)) ? $up->nickname : null
|
||||
'nickname' => ($up = User::find($texture->uploader)) ? $up->nickname : null,
|
||||
'report' => intval(option('reporter_score_modification', 0)),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -185,6 +186,7 @@ class SkinlibController extends Controller
|
|||
'scorePublic' => intval(option('score_per_storage')),
|
||||
'scorePrivate' => intval(option('private_score_per_storage')),
|
||||
'award' => intval(option('score_award_per_texture')),
|
||||
'contentPolicy' => app('parsedown')->text(option_localized('content_policy')),
|
||||
])
|
||||
->with('with_out_filter', true);
|
||||
}
|
||||
|
|
|
|||
32
app/Models/Report.php
Normal file
32
app/Models/Report.php
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Report extends Model
|
||||
{
|
||||
public const CREATED_AT = 'report_at';
|
||||
public const UPDATED_AT = null;
|
||||
|
||||
public const PENDING = 0;
|
||||
public const RESOLVED = 1;
|
||||
public const REJECTED = 2;
|
||||
|
||||
protected $casts = [
|
||||
'tid' => 'integer',
|
||||
'uploader' => 'integer',
|
||||
'reporter' => 'integer',
|
||||
'status' => 'integer',
|
||||
];
|
||||
|
||||
public function texture()
|
||||
{
|
||||
return $this->belongsTo(Texture::class, 'tid', 'tid');
|
||||
}
|
||||
|
||||
public function informer()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'reporter', 'uid');
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ $menu['user'] = [
|
|||
['title' => 'general.dashboard', 'link' => 'user', 'icon' => 'fa-tachometer-alt'],
|
||||
['title' => 'general.my-closet', 'link' => 'user/closet', 'icon' => 'fa-star'],
|
||||
['title' => 'general.player-manage', 'link' => 'user/player', 'icon' => 'fa-users'],
|
||||
['title' => 'general.my-reports', 'link' => 'user/reports', 'icon' => 'fa-flag'],
|
||||
['title' => 'general.profile', 'link' => 'user/profile', 'icon' => 'fa-user'],
|
||||
];
|
||||
|
||||
|
|
@ -19,6 +20,7 @@ $menu['admin'] = [
|
|||
['title' => 'general.dashboard', 'link' => 'admin', 'icon' => 'fa-tachometer-alt'],
|
||||
['title' => 'general.user-manage', 'link' => 'admin/users', 'icon' => 'fa-users'],
|
||||
['title' => 'general.player-manage', 'link' => 'admin/players', 'icon' => 'fa-gamepad'],
|
||||
['title' => 'general.report-manage', 'link' => 'admin/reports', 'icon' => 'fa-flag'],
|
||||
['title' => 'general.customize', 'link' => 'admin/customize', 'icon' => 'fa-paint-brush'],
|
||||
['title' => 'general.score-options', 'link' => 'admin/score', 'icon' => 'fa-credit-card'],
|
||||
['title' => 'general.options', 'link' => 'admin/options', 'icon' => 'fa-cog'],
|
||||
|
|
|
|||
|
|
@ -53,4 +53,7 @@ return [
|
|||
'recaptcha_sitekey' => '',
|
||||
'recaptcha_secretkey' => '',
|
||||
'recaptcha_invisible' => 'false',
|
||||
'reporter_score_modification' => '0',
|
||||
'reporter_reward_score' => '0',
|
||||
'content_policy' => '',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateReportTable extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::create('reports', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->integer('tid');
|
||||
$table->integer('uploader');
|
||||
$table->integer('reporter');
|
||||
$table->longText('reason');
|
||||
$table->integer('status');
|
||||
$table->dateTime('report_at');
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('reports');
|
||||
}
|
||||
}
|
||||
|
|
@ -27,9 +27,9 @@ export default Vue.extend({
|
|||
this.serverParams.perPage = params.currentPerPage
|
||||
this.fetchData()
|
||||
},
|
||||
onSortChange(params: { sortType: 'asc' | 'desc', columnIndex: number }) {
|
||||
this.serverParams.sortType = params.sortType
|
||||
this.serverParams.sortField = this.columns[params.columnIndex].field
|
||||
onSortChange([params]: { type: 'asc' | 'desc', field: string }[]) {
|
||||
this.serverParams.sortType = params.type
|
||||
this.serverParams.sortField = params.field
|
||||
this.fetchData()
|
||||
},
|
||||
onSearch(params: { searchTerm: string }) {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,11 @@ export default [
|
|||
component: () => import('../views/user/Bind.vue'),
|
||||
el: 'form',
|
||||
},
|
||||
{
|
||||
path: 'user/reports',
|
||||
component: () => import('../views/user/Report.vue'),
|
||||
el: '.content',
|
||||
},
|
||||
{
|
||||
path: 'user/profile',
|
||||
component: () => import('../views/user/Profile.vue'),
|
||||
|
|
@ -42,6 +47,11 @@ export default [
|
|||
component: () => import('../views/admin/Players.vue'),
|
||||
el: '.content',
|
||||
},
|
||||
{
|
||||
path: 'admin/reports',
|
||||
component: () => import('../views/admin/Reports.vue'),
|
||||
el: '.content',
|
||||
},
|
||||
{
|
||||
path: 'admin/customize',
|
||||
component: () => import('../views/admin/Customization.vue'),
|
||||
|
|
|
|||
139
resources/assets/src/views/admin/Reports.vue
Normal file
139
resources/assets/src/views/admin/Reports.vue
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
<template>
|
||||
<section class="content">
|
||||
<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'">
|
||||
<el-button size="medium" @click="reject(props.row)">
|
||||
{{ $t('report.reject') }}
|
||||
</el-button>
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ props.formattedRow[props.column.field] }}
|
||||
</span>
|
||||
</template>
|
||||
</vue-good-table>
|
||||
</section>
|
||||
</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'
|
||||
|
||||
export default {
|
||||
name: 'ReportsManagement',
|
||||
components: {
|
||||
VueGoodTable,
|
||||
},
|
||||
mixins: [
|
||||
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.fetchData()
|
||||
},
|
||||
methods: {
|
||||
async fetchData() {
|
||||
const { data, totalRecords } = await this.$http.get(
|
||||
'/admin/report-data',
|
||||
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 {
|
||||
errno, msg, status,
|
||||
} = await this.$http.post(
|
||||
'/admin/reports',
|
||||
{ id: report.id, action }
|
||||
)
|
||||
if (errno === 0) {
|
||||
this.$message.success(msg)
|
||||
report.status = status
|
||||
} else {
|
||||
this.$message.warning(msg)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
@ -52,6 +52,14 @@
|
|||
:href="`${baseUrl}/raw/${tid}.png`"
|
||||
:download="`${name}`.png"
|
||||
/>
|
||||
<el-button
|
||||
type="warning"
|
||||
size="medium"
|
||||
data-test="report"
|
||||
@click="report"
|
||||
>
|
||||
{{ $t('skinlib.report.title') }}
|
||||
</el-button>
|
||||
</template>
|
||||
<div
|
||||
class="btn likes pull-right"
|
||||
|
|
@ -181,6 +189,7 @@ export default {
|
|||
currentUid: blessing.extra.currentUid,
|
||||
admin: blessing.extra.admin,
|
||||
uploaderNickName: blessing.extra.nickname,
|
||||
reportScore: blessing.extra.report,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -320,6 +329,35 @@ export default {
|
|||
this.$message.warning(msg)
|
||||
}
|
||||
},
|
||||
async report() {
|
||||
const message = (() => {
|
||||
if (this.reportScore > 0) {
|
||||
return this.$t('skinlib.report.positive', { score: this.reportScore })
|
||||
} else if (this.reportScore < 0) {
|
||||
return this.$t('skinlib.report.negative', { score: -this.reportScore })
|
||||
}
|
||||
return ''
|
||||
})()
|
||||
let reason
|
||||
try {
|
||||
({ value: reason } = await this.$prompt(message, {
|
||||
title: this.$t('skinlib.report.title'),
|
||||
inputPlaceholder: this.$t('skinlib.report.reason'),
|
||||
}))
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const { errno, msg } = await this.$http.post(
|
||||
'/skinlib/report',
|
||||
{ tid: this.tid, reason }
|
||||
)
|
||||
if (errno === 0) {
|
||||
this.$message.success(msg)
|
||||
} else {
|
||||
this.$message.warning(msg)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -46,7 +46,11 @@
|
|||
{{ $t('skinlib.upload.remove') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div><!-- /.box-body -->
|
||||
|
||||
<a href="#" @click="showContentPolicy">
|
||||
{{ $t('skinlib.showContentPolicy') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="box-footer">
|
||||
<el-switch
|
||||
|
|
@ -118,6 +122,7 @@ export default {
|
|||
scorePublic: blessing.extra.scorePublic,
|
||||
scorePrivate: blessing.extra.scorePrivate,
|
||||
award: blessing.extra.award,
|
||||
contentPolicy: blessing.extra.contentPolicy,
|
||||
width2d: 64,
|
||||
}
|
||||
},
|
||||
|
|
@ -186,6 +191,11 @@ export default {
|
|||
this.$refs.upload.clear()
|
||||
this.texture = ''
|
||||
},
|
||||
showContentPolicy() {
|
||||
this.$alert(this.contentPolicy, {
|
||||
dangerouslyUseHTMLString: true,
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
80
resources/assets/src/views/user/Report.vue
Normal file
80
resources/assets/src/views/user/Report.vue
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
<template>
|
||||
<section class="content">
|
||||
<vue-good-table
|
||||
:rows="reports"
|
||||
:columns="columns"
|
||||
:search-options="tableOptions.search"
|
||||
:pagination-options="tableOptions.pagination"
|
||||
style-class="vgt-table striped"
|
||||
>
|
||||
<template #table-row="props">
|
||||
<span v-if="props.column.field === 'tid'">
|
||||
{{ props.formattedRow[props.column.field] }}
|
||||
<a :href="`${baseUrl}/skinlib/show/${props.row.tid}`">
|
||||
<i class="fa fa-share" />
|
||||
</a>
|
||||
</span>
|
||||
<span v-else-if="props.column.field === 'status'">
|
||||
{{ $t(`report.status.${props.row.status}`) }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ props.formattedRow[props.column.field] }}
|
||||
</span>
|
||||
</template>
|
||||
</vue-good-table>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { VueGoodTable } from 'vue-good-table'
|
||||
import 'vue-good-table/dist/vue-good-table.min.css'
|
||||
import tableOptions from '../../components/mixins/tableOptions'
|
||||
|
||||
export default {
|
||||
name: 'MyReports',
|
||||
components: {
|
||||
VueGoodTable,
|
||||
},
|
||||
mixins: [
|
||||
tableOptions,
|
||||
],
|
||||
props: {
|
||||
baseUrl: {
|
||||
type: String,
|
||||
default: blessing.base_url,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
reports: [],
|
||||
columns: [
|
||||
{
|
||||
field: 'id', label: 'ID', type: 'number',
|
||||
},
|
||||
{
|
||||
field: 'tid', label: this.$t('report.tid'), type: 'number',
|
||||
},
|
||||
{
|
||||
field: 'reason',
|
||||
label: this.$t('report.reason'),
|
||||
sortable: false,
|
||||
},
|
||||
{ field: 'status', label: this.$t('report.status-title') },
|
||||
{
|
||||
field: 'report_at',
|
||||
label: this.$t('report.time'),
|
||||
globalSearchDisabled: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchData()
|
||||
},
|
||||
methods: {
|
||||
async fetchData() {
|
||||
this.reports = await this.$http.get('/user/report-list')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
@ -27,7 +27,7 @@ test('change sort type', () => {
|
|||
{ field: '2' },
|
||||
],
|
||||
})
|
||||
wrapper.vm.onSortChange({ sortType: 'desc', columnIndex: 2 })
|
||||
wrapper.vm.onSortChange([{ type: 'desc', field: '2' }])
|
||||
expect(wrapper.vm.fetchData).toBeCalled()
|
||||
expect(wrapper.vm.serverParams.sortType).toBe('desc')
|
||||
expect(wrapper.vm.serverParams.sortField).toBe('2')
|
||||
|
|
|
|||
99
resources/assets/tests/views/admin/Reports.test.ts
Normal file
99
resources/assets/tests/views/admin/Reports.test.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import Vue from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Reports from '@/views/admin/Reports.vue'
|
||||
import { flushPromises } from '../../utils'
|
||||
|
||||
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 wrapper.vm.$nextTick()
|
||||
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 wrapper.vm.$nextTick()
|
||||
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({ errno: 1, msg: 'fail' })
|
||||
.mockResolvedValue({
|
||||
errno: 0, msg: 'ok', status: 1,
|
||||
})
|
||||
const wrapper = mount(Reports)
|
||||
await wrapper.vm.$nextTick()
|
||||
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(Vue.prototype.$message.warning).toBeCalledWith('fail')
|
||||
|
||||
button.trigger('click')
|
||||
await flushPromises()
|
||||
expect(Vue.prototype.$message.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({
|
||||
errno: 0, msg: 'ok', status: 1,
|
||||
})
|
||||
const wrapper = mount(Reports)
|
||||
await wrapper.vm.$nextTick()
|
||||
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(Vue.prototype.$message.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({
|
||||
errno: 0, msg: 'ok', status: 2,
|
||||
})
|
||||
const wrapper = mount(Reports)
|
||||
await wrapper.vm.$nextTick()
|
||||
const button = wrapper.find('button')
|
||||
|
||||
button.trigger('click')
|
||||
await flushPromises()
|
||||
expect(Vue.prototype.$http.post).toBeCalledWith(
|
||||
'/admin/reports',
|
||||
{ id: 1, action: 'reject' }
|
||||
)
|
||||
expect(Vue.prototype.$message.success).toBeCalledWith('ok')
|
||||
expect(wrapper.text()).toContain('report.status.2')
|
||||
})
|
||||
|
|
@ -358,3 +358,52 @@ test('delete texture', async () => {
|
|||
jest.runAllTimers()
|
||||
expect(Vue.prototype.$message.success).toBeCalledWith('0')
|
||||
})
|
||||
|
||||
test('report texture', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({ report: 0 })
|
||||
Vue.prototype.$http.post
|
||||
.mockResolvedValueOnce({ errno: 1, msg: 'duplicated' })
|
||||
.mockResolvedValue({ errno: 0, msg: 'success' })
|
||||
Vue.prototype.$prompt
|
||||
.mockRejectedValueOnce('')
|
||||
.mockRejectedValueOnce('')
|
||||
.mockResolvedValue({ value: 'reason' } as MessageBoxData)
|
||||
const wrapper = mount(Show, {
|
||||
mocks: {
|
||||
$route: ['/skinlib/show/1', '1'],
|
||||
},
|
||||
stubs: { previewer },
|
||||
})
|
||||
|
||||
const button = wrapper.find('[data-test=report]')
|
||||
button.trigger('click')
|
||||
expect(Vue.prototype.$prompt).toBeCalledWith('', {
|
||||
title: 'skinlib.report.title',
|
||||
inputPlaceholder: 'skinlib.report.reason',
|
||||
})
|
||||
expect(Vue.prototype.$http.post).not.toBeCalled()
|
||||
|
||||
wrapper.setData({ reportScore: -5 })
|
||||
button.trigger('click')
|
||||
expect(Vue.prototype.$prompt).toBeCalledWith('skinlib.report.negative', {
|
||||
title: 'skinlib.report.title',
|
||||
inputPlaceholder: 'skinlib.report.reason',
|
||||
})
|
||||
|
||||
wrapper.setData({ reportScore: 5 })
|
||||
button.trigger('click')
|
||||
expect(Vue.prototype.$prompt).toBeCalledWith('skinlib.report.positive', {
|
||||
title: 'skinlib.report.title',
|
||||
inputPlaceholder: 'skinlib.report.reason',
|
||||
})
|
||||
await flushPromises()
|
||||
expect(Vue.prototype.$http.post).toBeCalledWith(
|
||||
'/skinlib/report',
|
||||
{ tid: 1, reason: 'reason' }
|
||||
)
|
||||
expect(Vue.prototype.$message.warning).toBeCalledWith('duplicated')
|
||||
|
||||
button.trigger('click')
|
||||
await flushPromises()
|
||||
expect(Vue.prototype.$message.success).toBeCalledWith('success')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ window.blessing.extra = {
|
|||
scorePrivate: 10,
|
||||
scorePublic: 1,
|
||||
award: 0,
|
||||
contentPolicy: 'the policy',
|
||||
}
|
||||
|
||||
const csrf = document.createElement('meta')
|
||||
|
|
@ -167,3 +168,11 @@ test('show notice about awarding', () => {
|
|||
wrapper.find('[type=checkbox]').setChecked()
|
||||
expect(wrapper.find('.callout-success').exists()).toBeFalse()
|
||||
})
|
||||
|
||||
test('show content policy', () => {
|
||||
const wrapper = mount(Upload)
|
||||
wrapper.find('a').trigger('click')
|
||||
expect(Vue.prototype.$alert).toBeCalledWith('the policy', {
|
||||
dangerouslyUseHTMLString: true,
|
||||
})
|
||||
})
|
||||
|
|
|
|||
16
resources/assets/tests/views/user/Report.test.ts
Normal file
16
resources/assets/tests/views/user/Report.test.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import Vue from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Report from '@/views/user/Report.vue'
|
||||
|
||||
test('basic render', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue([
|
||||
{
|
||||
id: 1, tid: 1, reason: 'abc', status: 1,
|
||||
},
|
||||
])
|
||||
const wrapper = mount(Report)
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.find('a').attributes('href')).toBe('/skinlib/show/1')
|
||||
expect(wrapper.text()).toContain('report.status.1')
|
||||
})
|
||||
|
|
@ -146,4 +146,6 @@ update:
|
|||
unzip: "Failed to extract update package. Error code: "
|
||||
overwrite: Unable to overwrite files.
|
||||
|
||||
report-reviewed: This report has been processed.
|
||||
|
||||
invalid-action: Invalid action
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ skinlib:
|
|||
setNewTextureName: 'Please enter the new texture name:'
|
||||
emptyNewTextureName: Empty new texture name.
|
||||
seeMyUpload: My Uploads
|
||||
apply: Quick Apply
|
||||
apply: Apply
|
||||
filter:
|
||||
skin: (Any Model)
|
||||
steve: (Steve)
|
||||
|
|
@ -69,6 +69,7 @@ skinlib:
|
|||
emptyUploadFile: You have not uploaded any file.
|
||||
encodingError: 'Error: Encoding of this file is not accepted.'
|
||||
fileExtError: 'Error: Textures should be PNG files.'
|
||||
showContentPolicy: Read content policy
|
||||
uploading: Uploading
|
||||
redirecting: Redirecting...
|
||||
setAsPrivate: Set as Private
|
||||
|
|
@ -98,9 +99,14 @@ skinlib:
|
|||
size: File Size
|
||||
uploader: Uploader
|
||||
upload-at: Upload At
|
||||
download: Download Texture
|
||||
download: Download
|
||||
delete-texture: Delete Texture
|
||||
manage-notice: The texture which was deleted or setted to private will be removed from the closet of everyone who had favorited it.
|
||||
report:
|
||||
title: Report
|
||||
reason: Tell us reason please.
|
||||
positive: To encourage positive contributions to the skinlib, we will reward who reported inappropriate content with :score scores. However, if any malicious reporting behaviors were found, all scores rewarded will be taken back.
|
||||
negative: To mitigate the impact of malicious reports, we will require :score scores for submitting a texture report. Don't worry. The suspended scores and additional reward will be sent to your account after your report reviewed by administrators.
|
||||
|
||||
user:
|
||||
signRemainingTime: 'Available after :time :unit'
|
||||
|
|
@ -309,6 +315,21 @@ admin:
|
|||
black: Black
|
||||
black-light: Black Light
|
||||
|
||||
report:
|
||||
tid: Texture ID
|
||||
reporter: Reporter
|
||||
reason: Reason
|
||||
status-title: Status
|
||||
status:
|
||||
- Pending
|
||||
- Resolved
|
||||
- Rejected
|
||||
time: Report Time
|
||||
check: Details
|
||||
delete: Delete
|
||||
ban: Ban
|
||||
reject: Reject
|
||||
|
||||
general:
|
||||
skin: Skin
|
||||
cape: Cape
|
||||
|
|
|
|||
|
|
@ -12,8 +12,10 @@ anonymous: Guest
|
|||
back: Back
|
||||
dashboard: Dashboard
|
||||
my-closet: Closet
|
||||
my-reports: Reports
|
||||
player-manage: Players
|
||||
user-manage: Users
|
||||
report-manage: Reports
|
||||
plugin-manage: Plugins
|
||||
plugin-market: Plugin Market
|
||||
plugin-configs: Plugin Configs
|
||||
|
|
@ -37,6 +39,7 @@ pause: Pause
|
|||
reset: Reset
|
||||
|
||||
submit: Submit
|
||||
op-success: Operated successfully.
|
||||
|
||||
notice: Notice
|
||||
switch-2d-preview: Switch to 2D preview
|
||||
|
|
|
|||
|
|
@ -47,6 +47,15 @@ rate:
|
|||
addon: scores = 1 player
|
||||
user_initial_score: User Initial Score
|
||||
|
||||
report:
|
||||
title: Reporting Textures
|
||||
|
||||
reporter_score_modification:
|
||||
title: Scores for Submitting an Report
|
||||
description: Set a positive integer value to reward user who submits new reports. Set to a negative value will require scores for submitting reports, and the suspended scores will be available if user's report was resolved. Set to 0 to disable.
|
||||
reporter_reward_score:
|
||||
title: Reward the Reporter with Scores If Report Resolved
|
||||
|
||||
sign:
|
||||
title: Signing
|
||||
|
||||
|
|
@ -125,6 +134,9 @@ general:
|
|||
title: Texture Name Rules
|
||||
hint: The RegExp for validating name of uploaded textures. Leave empty to allow any character except single, double quote and backslash.
|
||||
placeholder: Regular Expressions
|
||||
content_policy:
|
||||
title: Content Policy
|
||||
description: Display content policy at texture uploading page, supporting Markdown. To edit a specific language's corresponding content policy, please switch to that language and submit your edit.
|
||||
comment_script:
|
||||
title: Comment Script
|
||||
description: Placeholder is available, <code>{tid}</code> will be replaced with texture id, <code>{name}</code> will be replaced with texture name, <code>{url}</code> will be replaced with current URL.
|
||||
|
|
|
|||
|
|
@ -94,3 +94,7 @@ model:
|
|||
|
||||
no-permission: You have no permission to moderate this texture.
|
||||
non-existent: No such texture.
|
||||
|
||||
report:
|
||||
duplicate: You have already reported this texture. The administrators will review it as soon as possible. You can also track the status of your report at User Center.
|
||||
success: Thanks for reporting! The administrators will review it as soon as possible.
|
||||
|
|
|
|||
|
|
@ -151,4 +151,6 @@ update:
|
|||
unzip: 更新包解压缩失败。错误代码:
|
||||
overwrite: 你的服务器不支持自动更新:无法覆盖文件。
|
||||
|
||||
report-reviewed: 这一条举报已经处理过了。
|
||||
|
||||
invalid-action: 无效的操作名
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ skinlib:
|
|||
emptyUploadFile: 你还没有上传任何文件哦
|
||||
encodingError: 错误:这张图片编码不对哦
|
||||
fileExtError: 错误:皮肤文件必须为 PNG 格式
|
||||
showContentPolicy: 查看内容策略
|
||||
uploading: 上传中
|
||||
redirecting: 正在跳转...
|
||||
setAsPrivate: 设为隐私
|
||||
|
|
@ -100,9 +101,14 @@ skinlib:
|
|||
size: 文件大小
|
||||
uploader: 上传者
|
||||
upload-at: 上传日期
|
||||
download: 下载材质
|
||||
download: 下载
|
||||
delete-texture: 删除材质
|
||||
manage-notice: 材质设为隐私或被删除后将会从每一个收藏者的衣柜中移除。
|
||||
report:
|
||||
title: 举报
|
||||
reason: 请填写举报原因
|
||||
positive: 为鼓励用户积极维护皮肤库的环境,每举报一个材质你可以获得 :score 积分的奖励。但是,如果被发现有恶意举报等行为,奖励的积分将会被全部收回,并且有可能受到额外的惩罚。
|
||||
negative: 为了减轻恶意举报带来的工作量,我们会在你提交举报时扣除 :score 积分。不用担心,举报通过后暂扣的积分将会全部返还,并且可以获得额外的积分奖励。
|
||||
|
||||
user:
|
||||
signRemainingTime: ':time :unit 后可签到'
|
||||
|
|
@ -303,6 +309,21 @@ admin:
|
|||
black: 高端黑
|
||||
black-light: 黑色主题 - 白色侧边栏
|
||||
|
||||
report:
|
||||
tid: 材质 ID
|
||||
reporter: 举报人
|
||||
reason: 举报原因
|
||||
status-title: 状态
|
||||
status:
|
||||
- 正在处理
|
||||
- 处理完成
|
||||
- 已被拒绝
|
||||
time: 举报时间
|
||||
check: 查看
|
||||
delete: 删除
|
||||
ban: 封禁
|
||||
reject: 拒绝举报
|
||||
|
||||
general:
|
||||
skin: 皮肤
|
||||
cape: 披风
|
||||
|
|
|
|||
|
|
@ -12,8 +12,10 @@ anonymous: 未登录
|
|||
back: 返回
|
||||
dashboard: 仪表盘
|
||||
my-closet: 我的衣柜
|
||||
my-reports: 我的举报
|
||||
player-manage: 角色管理
|
||||
user-manage: 用户管理
|
||||
report-manage: 举报管理
|
||||
plugin-manage: 插件管理
|
||||
plugin-market: 插件市场
|
||||
plugin-configs: 插件配置
|
||||
|
|
@ -37,6 +39,7 @@ pause: 暂停
|
|||
reset: 重置
|
||||
|
||||
submit: 提交
|
||||
op-success: 操作成功
|
||||
|
||||
notice: 提示
|
||||
switch-2d-preview: 切换 2D 预览
|
||||
|
|
|
|||
|
|
@ -47,6 +47,15 @@ rate:
|
|||
addon: 积分 = 一个角色
|
||||
user_initial_score: 新用户默认积分
|
||||
|
||||
report:
|
||||
title: 材质举报
|
||||
|
||||
reporter_score_modification:
|
||||
title: 提交举报所需积分
|
||||
description: 举报材质时【奖励】或者【扣除】举报者一定的积分。设置为正数表示奖励相应积分,设置为负数时表示扣除相应积分,设置为 0 可关闭本功能。举报时扣除积分可以一定程度上减少恶意举报,如果举报通过后,扣除的积分将会被返还。
|
||||
reporter_reward_score:
|
||||
title: 举报通过后奖励积分
|
||||
|
||||
sign:
|
||||
title: 签到配置
|
||||
|
||||
|
|
@ -125,6 +134,9 @@ general:
|
|||
title: 材质名称规则
|
||||
hint: 皮肤库上传材质时名称的正则表达式。留空表示允许使用除半角单双引号、反斜杠以外的任意字符。
|
||||
placeholder: 正则表达式,不懂别乱填
|
||||
content_policy:
|
||||
title: 材质内容策略
|
||||
description: 在材质上传页面将会显示此内容,支持 Markdown。如果想要编辑某种特定语言下的内容政策,请在右上角切换至该语言后再提交修改。
|
||||
comment_script:
|
||||
title: 评论代码
|
||||
description: 评论代码内可使用占位符,<code>{tid}</code> 将会被自动替换为材质的 id,<code>{name}</code> 会被替换为材质名称,<code>{url}</code> 会被替换为当前页面地址。
|
||||
|
|
|
|||
|
|
@ -93,3 +93,7 @@ model:
|
|||
|
||||
no-permission: 你没有权限修改此材质
|
||||
non-existent: 材质不存在
|
||||
|
||||
report:
|
||||
duplicate: 您已经举报过该材质了,请耐心等待管理员处理。您可以在用户中心查看举报的处理进度。
|
||||
success: 举报已提交,请等待管理员处理
|
||||
|
|
|
|||
12
resources/views/admin/reports.blade.php
Normal file
12
resources/views/admin/reports.blade.php
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
@extends('admin.master')
|
||||
|
||||
@section('title', trans('general.report-manage'))
|
||||
|
||||
@section('content')
|
||||
<div class="content-wrapper">
|
||||
<section class="content-header">
|
||||
<h1>@lang('general.report-manage')</h1>
|
||||
</section>
|
||||
<section class="content"></section>
|
||||
</div>
|
||||
@endsection
|
||||
|
|
@ -18,6 +18,8 @@
|
|||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{!! $forms['rate']->render() !!}
|
||||
|
||||
{!! $forms['report']->render() !!}
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
|
|
|
|||
12
resources/views/user/report.blade.php
Normal file
12
resources/views/user/report.blade.php
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
@extends('user.master')
|
||||
|
||||
@section('title', trans('general.my-reports'))
|
||||
|
||||
@section('content')
|
||||
<div class="content-wrapper">
|
||||
<section class="content-header">
|
||||
<h1>@lang('general.my-reports')</h1>
|
||||
</section>
|
||||
<section class="content"></section>
|
||||
</div>
|
||||
@endsection
|
||||
|
|
@ -49,6 +49,9 @@ Route::group([
|
|||
Route::get('/score-info', 'UserController@scoreInfo');
|
||||
Route::post('/sign', 'UserController@sign');
|
||||
|
||||
Route::get('/reports', 'ReportController@viewTrack');
|
||||
Route::get('/report-list', 'ReportController@track');
|
||||
|
||||
// Profile
|
||||
Route::get('/profile', 'UserController@profile');
|
||||
Route::post('/profile', 'UserController@handleProfile');
|
||||
|
|
@ -95,6 +98,7 @@ Route::group(['prefix' => 'skinlib'], function () {
|
|||
Route::post('/rename', 'SkinlibController@rename');
|
||||
Route::post('/privacy', 'SkinlibController@privacy');
|
||||
Route::post('/delete', 'SkinlibController@delete');
|
||||
Route::post('/report', 'ReportController@submit');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -111,15 +115,17 @@ Route::group(['middleware' => ['auth', 'admin'], 'prefix' => 'admin'], function
|
|||
Route::any('/resource', 'AdminController@resource');
|
||||
|
||||
Route::view('/users', 'admin.users');
|
||||
Route::post('/users', 'AdminController@userAjaxHandler');
|
||||
Route::any('/user-data', 'AdminController@getUserData');
|
||||
|
||||
Route::view('/players', 'admin.players');
|
||||
Route::post('/players', 'AdminController@playerAjaxHandler');
|
||||
Route::any('/player-data', 'AdminController@getPlayerData');
|
||||
Route::get('/user/{uid}', 'AdminController@getOneUser');
|
||||
|
||||
// ajax handlers
|
||||
Route::post('/users', 'AdminController@userAjaxHandler');
|
||||
Route::post('/players', 'AdminController@playerAjaxHandler');
|
||||
Route::view('/reports', 'admin.reports');
|
||||
Route::post('/reports', 'ReportController@review');
|
||||
Route::any('/report-data', 'ReportController@manage');
|
||||
|
||||
Route::group(['prefix' => 'plugins', 'middleware' => 'super-admin'], function () {
|
||||
Route::get('/data', 'PluginController@getPluginData');
|
||||
|
|
|
|||
|
|
@ -87,6 +87,13 @@ class AdminControllerTest extends BrowserKitTestCase
|
|||
$this->assertEquals('12', option('score_per_player'));
|
||||
$this->assertEquals('500', option('user_initial_score'));
|
||||
|
||||
$this->visit('/admin/score')
|
||||
->type('1', 'reporter_score_modification')
|
||||
->type('2', 'reporter_reward_score')
|
||||
->press('submit_report');
|
||||
$this->assertEquals('1', option('reporter_score_modification'));
|
||||
$this->assertEquals('2', option('reporter_reward_score'));
|
||||
|
||||
$this->visit('/admin/score')
|
||||
->type('233', 'sign_score_from')
|
||||
->type('666', 'sign_score_to')
|
||||
|
|
@ -125,6 +132,7 @@ class AdminControllerTest extends BrowserKitTestCase
|
|||
->type('/^([0-9]+)$/', 'custom_player_name_regexp')
|
||||
->select('1', 'api_type')
|
||||
->check('auto_del_invalid_texture')
|
||||
->type('policy', 'content_policy')
|
||||
->type('code', 'comment_script')
|
||||
->press('submit_general');
|
||||
$this->assertEquals('My Site', option_localized('site_name'));
|
||||
|
|
@ -138,6 +146,7 @@ class AdminControllerTest extends BrowserKitTestCase
|
|||
$this->assertEquals('/^([0-9]+)$/', option('custom_player_name_regexp'));
|
||||
$this->assertEquals('1', option('api_type'));
|
||||
$this->assertTrue(option('auto_del_invalid_texture'));
|
||||
$this->assertEquals('policy', option_localized('content_policy'));
|
||||
$this->assertEquals('code', option('comment_script'));
|
||||
|
||||
$this->visit('/admin/options')
|
||||
|
|
@ -148,7 +157,7 @@ class AdminControllerTest extends BrowserKitTestCase
|
|||
$this->visit('/admin/options')
|
||||
->type('announcement', 'announcement')
|
||||
->press('submit_announ');
|
||||
$this->assertEquals('announcement', option('announcement'));
|
||||
$this->assertEquals('announcement', option_localized('announcement'));
|
||||
|
||||
$this->visit('/admin/options')
|
||||
->type('kw', 'meta_keywords')
|
||||
|
|
|
|||
258
tests/ReportControllerTest.php
Normal file
258
tests/ReportControllerTest.php
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Report;
|
||||
use App\Models\Texture;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
class ReportControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function testSubmit()
|
||||
{
|
||||
$user = factory(User::class)->create();
|
||||
$texture = factory(Texture::class)->create();
|
||||
|
||||
// Without `tid` field
|
||||
$this->actAs($user)
|
||||
->postJson('/skinlib/report')
|
||||
->assertJson([
|
||||
'errno' => 1,
|
||||
'msg' => trans('validation.required', ['attribute' => 'tid'])
|
||||
]);
|
||||
|
||||
// Invalid texture
|
||||
$this->postJson('/skinlib/report', ['tid' => $texture->tid - 1])
|
||||
->assertJson([
|
||||
'errno' => 1,
|
||||
'msg' => trans('validation.exists', ['attribute' => 'tid'])
|
||||
]);
|
||||
|
||||
// Without `reason` field
|
||||
$this->postJson('/skinlib/report', ['tid' => $texture->tid])
|
||||
->assertJson([
|
||||
'errno' => 1,
|
||||
'msg' => trans('validation.required', ['attribute' => 'reason'])
|
||||
]);
|
||||
|
||||
// Lack of score
|
||||
$user->score = 0;
|
||||
$user->save();
|
||||
option(['reporter_score_modification' => -5]);
|
||||
$this->postJson('/skinlib/report', ['tid' => $texture->tid, 'reason' => 'reason'])
|
||||
->assertJson([
|
||||
'errno' => 1,
|
||||
'msg' => trans('skinlib.upload.lack-score')
|
||||
]);
|
||||
|
||||
// Success
|
||||
option(['reporter_score_modification' => 5]);
|
||||
$this->postJson('/skinlib/report', ['tid' => $texture->tid, 'reason' => 'reason'])
|
||||
->assertJson([
|
||||
'errno' => 0,
|
||||
'msg' => trans('skinlib.report.success')
|
||||
]);
|
||||
$user->refresh();
|
||||
$this->assertEquals(5, $user->score);
|
||||
$report = Report::where('reporter', $user->uid)->first();
|
||||
$this->assertEquals($texture->tid, $report->tid);
|
||||
$this->assertEquals($texture->uploader, $report->uploader);
|
||||
$this->assertEquals('reason', $report->reason);
|
||||
$this->assertEquals(Report::PENDING, $report->status);
|
||||
|
||||
// Prevent duplication
|
||||
$this->postJson('/skinlib/report', ['tid' => $texture->tid, 'reason' => 'reason'])
|
||||
->assertJson([
|
||||
'errno' => 1,
|
||||
'msg' => trans('skinlib.report.duplicate')
|
||||
]);
|
||||
}
|
||||
|
||||
public function testViewTrack()
|
||||
{
|
||||
$user = factory(User::class)->create();
|
||||
$this->actAs($user)->get('/user/reports')->assertViewIs('user.report');
|
||||
}
|
||||
|
||||
public function testTrack()
|
||||
{
|
||||
$user = factory(User::class)->create();
|
||||
$report = new Report;
|
||||
$report->tid = 1;
|
||||
$report->uploader = 0;
|
||||
$report->reporter = $user->uid;
|
||||
$report->reason = 'test';
|
||||
$report->status = Report::PENDING;
|
||||
$report->save();
|
||||
|
||||
$this->actAs($user)
|
||||
->getJson('/user/report-list')
|
||||
->assertJson([[
|
||||
'tid' => 1,
|
||||
'reason' => 'test',
|
||||
'status' => Report::PENDING,
|
||||
]]);
|
||||
}
|
||||
|
||||
public function testManage()
|
||||
{
|
||||
$uploader = factory(User::class)->create();
|
||||
$reporter = factory(User::class, 'admin')->create();
|
||||
$texture = factory(Texture::class)->create(['uploader' => $uploader->uid]);
|
||||
|
||||
$report = new Report;
|
||||
$report->tid = $texture->tid;
|
||||
$report->uploader = $uploader->uid;
|
||||
$report->reporter = $reporter->uid;
|
||||
$report->reason = 'test';
|
||||
$report->status = Report::PENDING;
|
||||
$report->save();
|
||||
|
||||
$this->actAs($reporter)
|
||||
->getJson('/admin/report-data')
|
||||
->assertJson([
|
||||
'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()
|
||||
{
|
||||
$uploader = factory(User::class)->create();
|
||||
$reporter = factory(User::class, 'admin')->create();
|
||||
$texture = factory(Texture::class)->create(['uploader' => $uploader->uid]);
|
||||
|
||||
$report = new Report;
|
||||
$report->tid = $texture->tid;
|
||||
$report->uploader = $uploader->uid;
|
||||
$report->reporter = $reporter->uid;
|
||||
$report->reason = 'test';
|
||||
$report->status = Report::REJECTED;
|
||||
$report->save();
|
||||
$report->refresh();
|
||||
|
||||
// Without `id` field
|
||||
$this->actAs($reporter)
|
||||
->postJson('/admin/reports')
|
||||
->assertJson([
|
||||
'errno' => 1,
|
||||
'msg' => trans('validation.required', ['attribute' => 'id'])
|
||||
]);
|
||||
|
||||
// Not existed
|
||||
$this->postJson('/admin/reports', ['id' => $report->id - 1])
|
||||
->assertJson([
|
||||
'errno' => 1,
|
||||
'msg' => trans('validation.exists', ['attribute' => 'id'])
|
||||
]);
|
||||
|
||||
// Without `action` field
|
||||
$this->postJson('/admin/reports', ['id' => $report->id])
|
||||
->assertJson([
|
||||
'errno' => 1,
|
||||
'msg' => trans('validation.required', ['attribute' => 'action'])
|
||||
]);
|
||||
|
||||
// Invalid action
|
||||
$this->postJson('/admin/reports', ['id' => $report->id, 'action' => 'a'])
|
||||
->assertJson([
|
||||
'errno' => 1,
|
||||
'msg' => trans('validation.in', ['attribute' => 'action'])
|
||||
]);
|
||||
|
||||
// Only process pending report
|
||||
$this->postJson('/admin/reports', ['id' => $report->id, 'action' => 'reject'])
|
||||
->assertJson([
|
||||
'errno' => 1,
|
||||
'msg' => trans('admin.report-reviewed')
|
||||
]);
|
||||
|
||||
// Reject
|
||||
$report->status = Report::PENDING;
|
||||
$report->save();
|
||||
$score = $reporter->score;
|
||||
$this->postJson('/admin/reports', ['id' => $report->id, 'action' => 'reject'])
|
||||
->assertJson([
|
||||
'errno' => 0,
|
||||
'msg' => trans('general.op-success'),
|
||||
'status' => Report::REJECTED
|
||||
]);
|
||||
$report->refresh();
|
||||
$reporter->refresh();
|
||||
$this->assertEquals(Report::REJECTED, $report->status);
|
||||
$this->assertEquals($score, $reporter->score);
|
||||
|
||||
$report->refresh();
|
||||
$report->status = Report::PENDING;
|
||||
$report->save();
|
||||
option(['reporter_score_modification' => 5]);
|
||||
$score = $reporter->score;
|
||||
$this->postJson('/admin/reports', ['id' => $report->id, 'action' => 'reject'])
|
||||
->assertJson(['errno' => 0]);
|
||||
$reporter->refresh();
|
||||
$this->assertEquals($score - 5, $reporter->score);
|
||||
|
||||
// Delete texture
|
||||
option(['reporter_score_modification' => -7]);
|
||||
$report->refresh();
|
||||
$report->status = Report::PENDING;
|
||||
$report->save();
|
||||
$score = $reporter->score;
|
||||
$this->postJson('/admin/reports', ['id' => $report->id, 'action' => 'delete'])
|
||||
->assertJson([
|
||||
'errno' => 0,
|
||||
'msg' => trans('general.op-success'),
|
||||
'status' => Report::RESOLVED
|
||||
]);
|
||||
$report->refresh();
|
||||
$reporter->refresh();
|
||||
$this->assertEquals(Report::RESOLVED, $report->status);
|
||||
$this->assertNull(Texture::find($texture->tid));
|
||||
$this->assertEquals($score + 7, $reporter->score);
|
||||
option(['reporter_score_modification' => 0]);
|
||||
|
||||
// Ban uploader
|
||||
option(['reporter_reward_score' => 6]);
|
||||
$report->refresh();
|
||||
$report->status = Report::PENDING;
|
||||
$report->reporter = $uploader->uid; // I REPORT MYSELF. (我 举 报 我 自 己)
|
||||
$report->save();
|
||||
$reporter = $uploader;
|
||||
$score = $reporter->score;
|
||||
$this->postJson('/admin/reports', ['id' => $report->id, 'action' => 'ban'])
|
||||
->assertJson([
|
||||
'errno' => 0,
|
||||
'msg' => trans('general.op-success'),
|
||||
'status' => Report::RESOLVED
|
||||
]);
|
||||
$reporter->refresh();
|
||||
$this->assertEquals(User::BANNED, $uploader->permission);
|
||||
$this->assertEquals($score + 6, $reporter->score);
|
||||
option(['reporter_reward_score' => 0]);
|
||||
|
||||
$report->refresh();
|
||||
$report->status = Report::PENDING;
|
||||
$report->save();
|
||||
$uploader->refresh();
|
||||
$uploader->permission = User::ADMIN;
|
||||
$uploader->save();
|
||||
$this->postJson('/admin/reports', ['id' => $report->id, 'action' => 'ban'])
|
||||
->assertJson([
|
||||
'errno' => 1,
|
||||
'msg' => trans('admin.users.operations.no-permission')
|
||||
]);
|
||||
$report->refresh();
|
||||
$this->assertEquals(Report::PENDING, $report->status);
|
||||
}
|
||||
}
|
||||
|
|
@ -34,7 +34,13 @@ class SetupControllerTest extends TestCase
|
|||
protected function dropAllTables()
|
||||
{
|
||||
$tables = [
|
||||
'user_closet', 'migrations', 'options', 'players', 'textures', 'users',
|
||||
'user_closet',
|
||||
'migrations',
|
||||
'options',
|
||||
'players',
|
||||
'textures',
|
||||
'users',
|
||||
'reports',
|
||||
];
|
||||
array_walk($tables, function ($table) {
|
||||
Schema::dropIfExists($table);
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user