Merge pull request #18426 from akemidx/global-report-templates

Fixed: Added global report templates
This commit is contained in:
snipe
2026-03-06 09:30:15 +01:00
committed by GitHub
13 changed files with 300 additions and 68 deletions

View File

@@ -19,7 +19,8 @@ class ReportTemplatesController extends Controller
$report = $request->user()->reportTemplates()->create([
'name' => $validated['name'],
'options' => $request->except(['_token', 'name']),
'options' => $request->except(['_token', 'name', 'is_shared']),
'is_shared' => $request->has('is_shared'),
]);
session()->flash('success', trans('admin/reports/message.create.success'));
@@ -45,6 +46,12 @@ class ReportTemplatesController extends Controller
{
$this->authorize('reports.view');
if ($reportTemplate->created_by != auth()->id()) {
return redirect()
->route('report-templates.show', $reportTemplate)
->withError(trans('general.report_not_editable'));
}
return view('reports/custom', [
'customfields' => CustomField::get(),
'template' => $reportTemplate,
@@ -55,13 +62,29 @@ class ReportTemplatesController extends Controller
{
$this->authorize('reports.view');
// Ignore "options" rules since data does not come in under that key...
$validated = $request->validate(Arr::except((new ReportTemplate)->getRules(), 'options'));
if ($reportTemplate->created_by != auth()->id()) {
return redirect()
->route('report-templates.show', $reportTemplate)
->withError(trans('general.report_not_editable'));
}
$reportTemplate->update([
'name' => $validated['name'],
'options' => $request->except(['_token', 'name']),
]);
$properties = [
'name' => $request->input('name'),
'options' => Arr::except($request->all(), ['_token', 'id', 'name', 'is_shared']),
'is_shared' => $reportTemplate->is_shared,
];
if ($reportTemplate->created_by == $request->user()->id) {
$properties['is_shared'] = $request->boolean('is_shared');
}
$reportTemplate->fill($properties);
if ($reportTemplate->isInvalid()) {
return redirect()->back()->withInput()->withErrors($reportTemplate->getErrors());
}
$reportTemplate->save();
session()->flash('success', trans('admin/reports/message.update.success'));
@@ -72,6 +95,12 @@ class ReportTemplatesController extends Controller
{
$this->authorize('reports.view');
if ($reportTemplate->creator()->isNot(auth()->user())) {
return redirect()
->route('report-templates.show', $reportTemplate)
->withError(trans('general.generic_model_not_found', ['model' => 'report template']));
}
$reportTemplate->delete();
return redirect()->route('reports/custom')

View File

@@ -453,6 +453,10 @@ class ReportsController extends Controller
$header = [];
if($request->filled('is_shared')) {
$header[] = trans('admin/reports/general.share_template');
}
if ($request->filled('id')) {
$header[] = trans('general.id');
}
@@ -659,7 +663,14 @@ class ReportsController extends Controller
$executionTime = microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'];
Log::debug('Added headers: '.$executionTime);
$assets = Asset::select('assets.*')->with(
if($request->filled('is_shared')) {
//to fill with logic for the report template and NOT the assets retrieved by the query
//do we scope here or??
}
$assets = Asset::select('assets.*')->with(
'location', 'assetstatus', 'company', 'defaultLoc', 'assignedTo',
'model.category', 'model.manufacturer', 'supplier');

View File

@@ -2,6 +2,7 @@
namespace App\Models;
use App\Http\Traits\UniqueUndeletedTrait;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
@@ -13,36 +14,47 @@ class ReportTemplate extends Model
{
use HasFactory;
use SoftDeletes;
use UniqueUndeletedTrait;
use ValidatingTrait;
protected $table = 'report_templates';
protected $casts = [
'options' => 'array',
'is_shared' => 'boolean',
];
protected $fillable = [
'created_by',
'name',
'options',
'is_shared',
];
protected $rules = [
'name' => [
'required',
'string',
'unique_undeleted:report_templates,name',
],
'options' => [
'required',
'array',
],
'is_shared' => [
'boolean',
],
];
protected static function booted()
{
// Scope to current user
// Scope to current user or if template is shared
static::addGlobalScope(
'current_user', function (Builder $builder) {
if (auth()->check()) {
$builder->where('created_by', auth()->id());
$builder->where('created_by', auth()->id())
->orWhere('is_shared', 1);
}
}
);

View File

@@ -20,6 +20,21 @@ class ReportTemplateFactory extends Factory
'id' => '1',
],
'created_by' => User::factory(),
'is_shared' => 0,
];
}
public function shared()
{
return $this->state(function () {
return['is_shared' => 1];
});
}
public function notShared()
{
return $this->state(function () {
return ['is_shared' => 0];
});
}
}

View File

@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('report_templates', function (Blueprint $table) {
$table->boolean('is_shared', )->nullable()->default(null);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('report_templates', function (Blueprint $table) {
if (Schema::hasColumn('report_templates', 'is_shared')) {
$table->dropColumn('is_shared');
}
});
}
};

View File

@@ -22,4 +22,8 @@ return [
'select_a_template' => 'Select a Template',
'template_name' => 'Template Name',
'update_template' => 'Update Template',
'share_template' => 'Share This Template',
'template_shared' => 'Template shared with you',
'template_shared_with_others' => 'Template shared with others',
'template_not_shared' => 'Template not shared with others',
];

View File

@@ -273,6 +273,7 @@ return [
'request_item' => 'Request this item',
'external_link_tooltip' => 'External link to',
'save' => 'Save',
'save_copy' => 'Save Copy',
'select_var' => 'Select :thing... ', // this will eventually replace all of our other selects
'select' => 'Select',
'select_all' => 'Select All',
@@ -711,6 +712,7 @@ return [
'select_all_none' => 'Select/Unselect All',
'generic_model_not_found' => 'That :model was not found or you do not have permission to access it',
'report_not_editable' => 'You do not have permission to edit this report template',
'deleted_models' => 'Deleted Asset Models',
'deleted_users' => 'Deleted Users',
'cost_each' => ':amount each',

View File

@@ -55,29 +55,33 @@
@endif
@if (request()->routeIs('report-templates.edit'))
<div class="row">
<div class="col-md-7 col-md-offset-4">
<div class="{{ $errors->has('name') ? ' has-error' : '' }}">
<label
for="name"
class="col-md-4 control-label"
>
{{ trans('admin/reports/general.template_name') }}
</label>
<div class="col-md-8">
<input
class="form-control"
placeholder=""
name="name"
type="text"
id="name"
value="{{ $template->name }}"
required
>
</div>
{!! $errors->first('name', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
</div>
<div class="form-group {{ $errors->has('name') ? ' has-error' : '' }}">
<label
for="name"
class="col-md-2 control-label"
>
{{ trans('admin/reports/general.template_name') }}
</label>
<div class="col-md-5">
<input
class="form-control"
placeholder=""
name="name"
type="text"
id="name"
value="{{ $template->name }}"
required
>
{!! $errors->first('name', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
</div>
@if ($template->created_by == auth()->id())
<div class="col-md-3">
<label class="form-control">
<input type="checkbox" name="is_shared" value="1" @checked($template->is_shared) />
{{ trans('admin/reports/general.share_template') }}
</label>
</div>
@endif
</div>
@endif
@@ -650,9 +654,9 @@
</div> <!-- /.box-body-->
<div class="box-footer text-right">
@if (request()->routeIs('report-templates.edit'))
<button type="submit" class="btn btn-primary">
<i class="fas fa-check icon-white" aria-hidden="true"></i>
@if(request()->routeIs('report-templates.edit'))
<button type="submit" class="btn btn-success">
<i class="fas fa-download icon-white" aria-hidden="true"></i>
{{ trans('general.save') }}
</button>
@else
@@ -691,36 +695,45 @@
<div class="row">
<div class="col-md-12">
@if (request()->routeIs('report-templates.show'))
<a
<div style="margin-bottom: 5px;">
@if($template->name)
@if($template->created_by == auth()->id())
<span class="text-center">{!! ($template->is_shared ? '<i class="fa fa-users"></i>'." ".(trans('admin/reports/general.template_shared_with_others')) : '<i class="fa fa-user"></i>'." ".(trans('admin/reports/general.template_not_shared')) )!!}</span>
@else
<span class="text-center">{!! ($template->is_shared ? '<i class="fa fa-users"></i>'." ".(trans('admin/reports/general.template_shared')) : '<i class="fa fa-user"></i>'." ".(trans('admin/reports/general.template_not_shared')) )!!}</span>
@endif
@endif
</div>
@if($template->created_by == auth()->id())
@if (request()->routeIs('report-templates.show'))
<a
href="{{ route('report-templates.edit', $template) }}"
class="btn btn-sm btn-warning btn-social btn-block"
data-tooltip="true"
title="{{ trans('admin/reports/general.update_template') }}"
style="margin-bottom: 5px;"
>
<x-icon type="edit" />
{{ trans('general.update') }}
</a>
<span data-tooltip="true" title="{{ trans('general.delete') }}">
<a href="#"
>
<x-icon type="edit" />
{{ trans('general.update') }}
</a>
<span data-tooltip="true" title="{{ trans('general.delete') }}">
<a href="#"
class="btn btn-sm btn-danger btn-social btn-block delete-asset"
data-toggle="modal"
data-title="{{ trans('general.delete') }}"
data-content="{{ trans('general.delete_confirm', ['item' => $template->name]) }}"
data-target="#dataConfirmModal"
type="button"
>
>
<x-icon type="delete" />
{{ trans('general.delete') }}
</a>
</span>
@endif
</a>
</span>
@endif
@endif
</div>
</div>
@endif

View File

@@ -17,9 +17,9 @@ class DeleteReportTemplateTest extends TestCase implements TestsPermissionsRequi
$this->actingAs(User::factory()->create())
->post(route('report-templates.destroy', $reportTemplate->id))
->assertStatus(302);
->assertRedirect(route('reports/custom'));
$this->assertModelExists($reportTemplate);
$this->assertNotSoftDeleted($reportTemplate);
}
public function testCannotDeleteAnotherUsersReportTemplate()
@@ -28,9 +28,22 @@ class DeleteReportTemplateTest extends TestCase implements TestsPermissionsRequi
$this->actingAs(User::factory()->canViewReports()->create())
->delete(route('report-templates.destroy', $reportTemplate->id))
->assertStatus(302);
->assertRedirect(route('reports/custom'))
->assertSessionHas('error', trans('general.generic_model_not_found', ['model' => 'report template']));
$this->assertModelExists($reportTemplate);
$this->assertNotSoftDeleted($reportTemplate);
}
public function testCannotDeleteAnotherUsersSharedReportTemplate()
{
$reportTemplate = ReportTemplate::factory()->shared()->create();
$this->actingAs(User::factory()->canViewReports()->create())
->delete(route('report-templates.destroy', $reportTemplate->id))
->assertRedirect(route('report-templates.show', $reportTemplate->id))
->assertSessionHas('error', trans('general.generic_model_not_found', ['model' => 'report template']));
$this->assertNotSoftDeleted($reportTemplate);
}
public function testCanDeleteAReportTemplate()

View File

@@ -15,7 +15,7 @@ class EditReportTemplateTest extends TestCase implements TestsPermissionsRequire
{
$this->actingAs(User::factory()->create())
->get(route('report-templates.edit', ReportTemplate::factory()->create()))
->assertStatus(302);
->assertRedirectToRoute('reports/custom');
}
public function testCannotLoadEditPageForAnotherUsersReportTemplate()
@@ -25,7 +25,17 @@ class EditReportTemplateTest extends TestCase implements TestsPermissionsRequire
$this->actingAs($user)
->get(route('report-templates.edit', $reportTemplate))
->assertStatus(302);
->assertRedirectToRoute('reports/custom');
}
public function testCannotLoadEditPageForAnotherUsersSharedReportTemplate()
{
$user = User::factory()->canViewReports()->create();
$reportTemplate = ReportTemplate::factory()->shared()->create();
$this->actingAs($user)
->get(route('report-templates.edit', $reportTemplate))
->assertRedirectToRoute('report-templates.show', $reportTemplate->id);
}
public function testCanLoadEditReportTemplatePage()

View File

@@ -15,7 +15,7 @@ class ShowReportTemplateTest extends TestCase implements TestsPermissionsRequire
{
$this->actingAs(User::factory()->create())
->get(route('report-templates.show', ReportTemplate::factory()->create()))
->assertStatus(302);
->assertRedirectToRoute('reports/custom');
}
public function testCanLoadASavedReportTemplate()
@@ -32,12 +32,29 @@ class ShowReportTemplateTest extends TestCase implements TestsPermissionsRequire
}]);
}
public function testCannotLoadAnotherUsersSavedReportTemplate()
public function testCannotLoadAnotherUsersSavedReportTemplateIfNotShared()
{
$reportTemplate = ReportTemplate::factory()->create();
$this->actingAs(User::factory()->canViewReports()->create())
->get(route('report-templates.show', $reportTemplate))
->assertStatus(302);
->assertRedirectToRoute('reports/custom')
->assertSessionHas('error', trans('general.generic_model_not_found', ['model' => 'report template']));;
}
public function testCanLoadAnotherUsersSavedReportTemplateIfShared()
{
$user = User::factory()->canViewReports()->create();
$reportTemplate = ReportTemplate::factory()->shared()->make(['name' => 'My Awesome Template']);
$user->reportTemplates()->save($reportTemplate);
$this->actingAs(User::factory()->canViewReports()->create())
->get(route('report-templates.show', $reportTemplate))
->assertOk()
->assertViewHas([
'template' => function (ReportTemplate $templatePassedToView) use ($reportTemplate) {
return $templatePassedToView->is($reportTemplate);
}
]);
}
}

View File

@@ -20,9 +20,31 @@ class UpdateReportTemplateTest extends TestCase implements TestsPermissionsRequi
public function testCannotUpdateAnotherUsersReportTemplate()
{
$reportTemplate = ReportTemplate::factory()->create(['name' => 'Original']);
$this->actingAs(User::factory()->canViewReports()->create())
->post(route('report-templates.update', ReportTemplate::factory()->create()))
->post(route('report-templates.update', $reportTemplate), [
'name' => 'Changed',
'options' => $reportTemplate->options,
])
->assertStatus(302);
$this->assertEquals('Original', $reportTemplate->fresh()->name);
}
public function testCannotUpdateAnotherUsersSharedReportTemplate()
{
$reportTemplate = ReportTemplate::factory()->shared()->create(['name' => 'Original']);
$this->actingAs(User::factory()->canViewReports()->create())
->post(route('report-templates.update', $reportTemplate), [
'name' => 'Changed',
'options' => $reportTemplate->options,
])
->assertStatus(302)
->assertSessionHas('error', trans('general.report_not_editable'));
$this->assertEquals('Original', $reportTemplate->fresh()->name);
}
public function testUpdatingReportTemplateRequiresValidFields()
@@ -33,7 +55,7 @@ class UpdateReportTemplateTest extends TestCase implements TestsPermissionsRequi
$this->actingAs($user)
->post(route('report-templates.update', $reportTemplate), [
//
'category' => 1,
])
->assertSessionHasErrors([
'name' => 'The name field is required.',
@@ -44,10 +66,9 @@ class UpdateReportTemplateTest extends TestCase implements TestsPermissionsRequi
{
$user = User::factory()->canViewReports()->create();
$reportTemplate = ReportTemplate::factory()->for($user, 'creator')->create([
$reportTemplate = ReportTemplate::factory()->notShared()->for($user, 'creator')->create([
'name' => 'Original Name',
'options' => [
'id' => 1,
'category' => 1,
'by_category_id' => 2,
'company' => 1,
@@ -58,17 +79,71 @@ class UpdateReportTemplateTest extends TestCase implements TestsPermissionsRequi
$this->actingAs($user)
->post(route('report-templates.update', $reportTemplate), [
'name' => 'Updated Name',
'id' => 1,
'company' => 1,
'by_company_id' => [3],
]);
])
->assertRedirectToRoute('report-templates.show', $reportTemplate->id);
$reportTemplate->refresh();
$this->assertEquals(0, $reportTemplate->is_shared);
$this->assertEquals('Updated Name', $reportTemplate->name);
$this->assertEquals(1, $reportTemplate->checkmarkValue('id'));
$this->assertEquals(0, $reportTemplate->checkmarkValue('category'));
$this->assertEquals([], $reportTemplate->selectValues('by_category_id'));
$this->assertEquals(1, $reportTemplate->checkmarkValue('company'));
$this->assertEquals([3], $reportTemplate->selectValues('by_company_id'));
}
public function testCanUpdateAReportTemplateWithTheSameName()
{
$user = User::factory()->canViewReports()->create();
$reportTemplate = ReportTemplate::factory()->notShared()->for($user, 'creator')->create([
'name' => 'Original Name',
'options' => [
'category' => 1,
'by_category_id' => 2,
'company' => 1,
'by_company_id' => [1, 2],
],
]);
$this->actingAs($user)
->post(route('report-templates.update', $reportTemplate), [
'name' => 'Original Name',
'company' => 1,
'by_company_id' => [3],
])
->assertSessionDoesntHaveErrors();
}
public function testCanShareAReportTemplate()
{
$user = User::factory()->canViewReports()->create();
$reportTemplate = ReportTemplate::factory()->notShared()->for($user, 'creator')->create([
'name' => 'Original Name',
'options' => [
'category' => 1,
'by_category_id' => 2,
'company' => 1,
],
]);
$this->actingAs($user)
->post(route('report-templates.update', $reportTemplate), [
'name' => 'Original Name',
'options' => [
'category' => 1,
'by_category_id' => 2,
'company' => 1,
],
'is_shared' => 1,
])
->assertRedirectToRoute('report-templates.show', $reportTemplate->id);
$reportTemplate->refresh();
$this->assertEquals(1, $reportTemplate->is_shared);
$this->assertEquals('Original Name', $reportTemplate->name);
}
}

View File

@@ -4,6 +4,7 @@ namespace Tests\Unit\Models\ReportTemplates;
use App\Models\Department;
use App\Models\Location;
use App\Models\User;
use App\Models\ReportTemplate;
use PHPUnit\Framework\Attributes\Group;
use Tests\TestCase;