Compare commits
98 Commits
v7.1.15
...
disallow_b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f98f978502 | ||
|
|
a0bbafeb30 | ||
|
|
dbd33e1a34 | ||
|
|
8afba32169 | ||
|
|
5014a95d9a | ||
|
|
5f0efd8cdb | ||
|
|
31155b5351 | ||
|
|
cc8f72c3f9 | ||
|
|
a0bab70def | ||
|
|
b5c8251539 | ||
|
|
0149ed75fd | ||
|
|
f72635955d | ||
|
|
5120cddda4 | ||
|
|
97398f1e68 | ||
|
|
281ff6ad5d | ||
|
|
9d49b01958 | ||
|
|
3f8916ea2e | ||
|
|
f6b9ae6aee | ||
|
|
de41def2b3 | ||
|
|
52b051e940 | ||
|
|
a137e31797 | ||
|
|
ca0f8ace99 | ||
|
|
6252f0ac5e | ||
|
|
43d66a8fcc | ||
|
|
e5284c03e2 | ||
|
|
16283b8fc0 | ||
|
|
983b78edd9 | ||
|
|
2ad1407d51 | ||
|
|
ab67c48352 | ||
|
|
14730184c9 | ||
|
|
8ff099e802 | ||
|
|
ea1f8328b9 | ||
|
|
99464fcd86 | ||
|
|
93d4d24194 | ||
|
|
1bbe9bf6c9 | ||
|
|
6874f703bf | ||
|
|
a7d5b3944e | ||
|
|
b5e83899c6 | ||
|
|
dcd586e3cd | ||
|
|
1246ab1de7 | ||
|
|
5a4a3aa8f3 | ||
|
|
2220828b00 | ||
|
|
716c67d4b8 | ||
|
|
ee4a54be24 | ||
|
|
46be1ada60 | ||
|
|
e1c0a80c20 | ||
|
|
2cb1b6d462 | ||
|
|
37ac7fe25c | ||
|
|
11f83b4cd9 | ||
|
|
0f1d10bd69 | ||
|
|
82108f8a18 | ||
|
|
a7dae10a82 | ||
|
|
2727210c78 | ||
|
|
fcbd5dcae5 | ||
|
|
fa80716320 | ||
|
|
15c9df0ab1 | ||
|
|
6ee0c93c87 | ||
|
|
37e091adbb | ||
|
|
021e82927e | ||
|
|
60642cd902 | ||
|
|
0d608552ef | ||
|
|
dd223fc215 | ||
|
|
9cb411c500 | ||
|
|
f0d3a6e2d3 | ||
|
|
5cb940c2ee | ||
|
|
299e743848 | ||
|
|
0c84904bf9 | ||
|
|
e00a1aec02 | ||
|
|
06e3bb7fd1 | ||
|
|
492e686b7a | ||
|
|
17706f150e | ||
|
|
6fef127cd1 | ||
|
|
f45b836010 | ||
|
|
925aea8531 | ||
|
|
bd6698de2a | ||
|
|
515f59fed9 | ||
|
|
5e74b109d9 | ||
|
|
8b643cb3b9 | ||
|
|
700647c53f | ||
|
|
081c5706c4 | ||
|
|
73a059c9ac | ||
|
|
4f9f035c69 | ||
|
|
cd3059f790 | ||
|
|
b69596a261 | ||
|
|
d02b9933e5 | ||
|
|
852a56fa9b | ||
|
|
9df78a9ed0 | ||
|
|
f3ad89931f | ||
|
|
21906d8c27 | ||
|
|
d262638a63 | ||
|
|
bebb72a04f | ||
|
|
77c5035cac | ||
|
|
2901ecbf43 | ||
|
|
27c120a55e | ||
|
|
4e43fa6b9f | ||
|
|
0f0baa207d | ||
|
|
3ff1745f56 | ||
|
|
552f90ae2c |
@@ -17,5 +17,3 @@ DB_PORT=3306
|
||||
DB_DATABASE=null
|
||||
DB_USERNAME=null
|
||||
DB_PASSWORD=null
|
||||
|
||||
MAIL_FROM_ADDR=you@example.com
|
||||
|
||||
38
.github/ISSUE_TEMPLATE.md
vendored
38
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,38 +0,0 @@
|
||||
#### Expected Behavior (or desired behavior if a feature request)
|
||||
|
||||
(what you expect to happen goes here)
|
||||
|
||||
-----
|
||||
|
||||
#### Actual Behavior
|
||||
|
||||
(what actually happens goes here)
|
||||
|
||||
-----
|
||||
|
||||
#### Please confirm you have done the following before posting your bug report:
|
||||
|
||||
- [ ] I have enabled debug mode
|
||||
- [ ] I have read [checked the Common Issues page](https://snipe-it.readme.io/docs/common-issues)
|
||||
|
||||
-----
|
||||
#### Provide answers to these questions:
|
||||
|
||||
- Is this a fresh install or an upgrade?
|
||||
- Version of Snipe-IT you're running
|
||||
- Version of PHP you're running
|
||||
- Version of MySQL/MariaDB you're running
|
||||
- What OS and web server you're running Snipe-IT on
|
||||
- What method you used to install Snipe-IT (install.sh, manual installation, docker, etc)
|
||||
- WITH DEBUG TURNED ON, if you're getting an error in your browser, include that error
|
||||
- What specific Snipe-IT page you're on, and what specific element you're interacting with to trigger the error
|
||||
- If a stacktrace is provided in the error, include that too.
|
||||
- Any errors that appear in your browser's error console.
|
||||
- Confirm whether the error is reproducible on the demo: https://snipeitapp.com/demo.
|
||||
- Include any additional information you can find in `storage/logs` and your webserver's logs.
|
||||
- Include what you've done so far in the installation, and if you got any error messages along the way.
|
||||
- Indicate whether or not you've manually edited any data directly in the database
|
||||
|
||||
Please do not post an issue without answering the related questions above. If you have opened a different issue and already answered these questions, answer them again, once for every ticket. It will be next to impossible for us to help you.
|
||||
|
||||
https://snipe-it.readme.io/docs/getting-help
|
||||
129
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
129
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,129 +0,0 @@
|
||||
name: Bug Report
|
||||
description: Create a report to help us improve
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Debug mode
|
||||
description: Please confirm you have done the following before posting your bug report
|
||||
options:
|
||||
- label: I have enabled debug mode
|
||||
required: true
|
||||
- label: I have read [checked the Common Issues page](https://snipe-it.readme.io/docs/common-issues)
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of what the bug is.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Reproduction steps
|
||||
description: Steps to reproduce the behavior.
|
||||
value: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: A clear and concise description of what you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: 'If applicable, add screenshots to help explain your problem.'
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "### Server"
|
||||
- type: input
|
||||
attributes:
|
||||
label: Snipe-IT Version
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: server_operatingSystem
|
||||
attributes:
|
||||
label: Operating System
|
||||
description: 'e.g. Ubuntu, Windows'
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Web Server
|
||||
description: 'e.g. Apache, IIS'
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: PHP Version
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "### Desktop"
|
||||
- type: input
|
||||
id: desktop_operatingSystem
|
||||
attributes:
|
||||
label: Operating System
|
||||
description: 'e.g. Ubuntu, Windows'
|
||||
- type: input
|
||||
id: desktop_browser
|
||||
attributes:
|
||||
label: Browser
|
||||
description: 'e.g. Google Chrome, Safari'
|
||||
- type: input
|
||||
id: desktop_version
|
||||
attributes:
|
||||
label: Version
|
||||
description: 'e.g. 93'
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "### Mobile"
|
||||
- type: input
|
||||
attributes:
|
||||
label: Device
|
||||
description: 'e.g. iPhone 6, Pixel 4a'
|
||||
- type: input
|
||||
id: mobile_operatingSystem
|
||||
attributes:
|
||||
label: Operating System
|
||||
description: 'e.g. iOS 8.1, Android 9'
|
||||
- type: input
|
||||
id: mobile_browser
|
||||
attributes:
|
||||
label: Browser
|
||||
description: 'e.g. Google Chrome, Safari'
|
||||
- type: input
|
||||
id: mobile_version
|
||||
attributes:
|
||||
label: Version
|
||||
description: 'e.g. 93'
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Error messages
|
||||
description: |
|
||||
WITH DEBUG TURNED ON, if you're getting an error in your browser, include that error
|
||||
If a stacktrace is provided in the error, include that too.
|
||||
Any errors that appear in your browser's error console.
|
||||
Confirm whether the error is reproducible on the demo: https://snipeitapp.com/demo.
|
||||
Include any additional information you can find in `storage/logs` and your webserver's logs.
|
||||
Include the output from `php -m` (this should display what modules you have enabled.)
|
||||
render: shell
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: |
|
||||
Is this a fresh install or an upgrade?
|
||||
What OS and web server you're running Snipe-IT on
|
||||
What method you used to install Snipe-IT (install.sh, manual installation, docker, etc)
|
||||
Include what you've done so far in the installation, and if you got any error messages along the way.
|
||||
Indicate whether or not you've manually edited any data directly in the database
|
||||
Add any other context about the problem here.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Please do not post an issue without answering the related questions above. If you have opened a different issue and already answered these questions, answer them again, once for every ticket. It will be next to impossible for us to help you.
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
1
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
25
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
25
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,25 +0,0 @@
|
||||
name: Feature Request
|
||||
description: Suggest an idea for this project
|
||||
title: "[Feature Request]: "
|
||||
labels: ["feature request"]
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Is your feature request related to a problem? Please describe.
|
||||
description: A clear and concise description of what the problem is. The more information you can provide about your use-case, the more liklely we are to consider your feature.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe alternatives you've considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
40
.github/pull_request_template.md
vendored
40
.github/pull_request_template.md
vendored
@@ -1,40 +0,0 @@
|
||||
# Description
|
||||
|
||||
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context, providing screenshots where practical. List any dependencies that are required for this change.
|
||||
|
||||
Fixes # (issue)
|
||||
|
||||
## Type of change
|
||||
|
||||
Please delete options that are not relevant.
|
||||
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] This change requires a documentation update
|
||||
|
||||
# How Has This Been Tested?
|
||||
|
||||
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
|
||||
|
||||
- [ ] Test A
|
||||
- [ ] Test B
|
||||
|
||||
**Test Configuration**:
|
||||
* PHP version:
|
||||
* MySQL version
|
||||
* Webserver version
|
||||
* OS version
|
||||
|
||||
|
||||
# Checklist:
|
||||
|
||||
- [ ] I have read the Contributing documentation available here: https://snipe-it.readme.io/docs/contributing-overview
|
||||
- [ ] I have formatted this PR according to the project guidelines: https://snipe-it.readme.io/docs/contributing-overview#pull-request-guidelines
|
||||
- [ ] My code follows the style guidelines of this project
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||
- [ ] I have made corresponding changes to the documentation
|
||||
- [ ] My changes generate no new warnings
|
||||
- [ ] I have added tests that prove my fix is effective or that my feature works
|
||||
- [ ] New and existing unit tests pass locally with my changes
|
||||
1
.github/travis-memory.ini
vendored
1
.github/travis-memory.ini
vendored
@@ -1 +0,0 @@
|
||||
memory_limit= 2048M
|
||||
7
.github/weekly-digest.yml
vendored
7
.github/weekly-digest.yml
vendored
@@ -1,7 +0,0 @@
|
||||
# Configuration for weekly-digest - https://github.com/apps/weekly-digest
|
||||
publishDay: sun
|
||||
canPublishIssues: true
|
||||
canPublishPullRequests: true
|
||||
canPublishContributors: true
|
||||
canPublishStargazers: true
|
||||
canPublishCommits: true
|
||||
@@ -10,10 +10,12 @@ however there are times when library dependencies and/or PHP/MySQL dependencies
|
||||
make it impossible to backport security fixes on older versions.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 5.1.x | :white_check_mark: |
|
||||
|---------| ------------------ |
|
||||
| 7.x | :white_check_mark: |
|
||||
| 6.x | :x: |
|
||||
| 5.1.x | :x: |
|
||||
| 5.0.x | :x: |
|
||||
| 4.0.x | :white_check_mark: |
|
||||
| 4.0.x | :x: |
|
||||
| < 4.0 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
@@ -427,7 +427,7 @@ class LdapSync extends Command
|
||||
$user->groups()->attach($ldap_default_group);
|
||||
}
|
||||
//updates assets location based on user's location
|
||||
Asset::where('assigned_to', '=', $user->id)->update(['location_id' => $user->location_id]);
|
||||
Asset::where('assigned_to', '=', $user->id)->where('assigned_type', '=', User::class)->update(['location_id' => $user->location_id]);
|
||||
|
||||
} else {
|
||||
foreach ($user->getErrors()->getMessages() as $key => $err) {
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Mail\UnacceptedAssetReminderMail;
|
||||
use App\Models\Asset;
|
||||
use App\Models\CheckoutAcceptance;
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use App\Notifications\CheckoutAssetNotification;
|
||||
use App\Notifications\CurrentInventory;
|
||||
use App\Notifications\UnacceptedAssetReminderNotification;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class SendAcceptanceReminder extends Command
|
||||
{
|
||||
@@ -65,42 +65,29 @@ class SendAcceptanceReminder extends Command
|
||||
return $item['acceptance']->assignedTo ? $item['acceptance']->assignedTo->id : '';
|
||||
});
|
||||
|
||||
$no_mail_address = [];
|
||||
|
||||
foreach($unacceptedAssetGroups as $unacceptedAssetGroup) {
|
||||
// The [0] is weird, but it allows for the item_count to work and grabs the appropriate info for each user.
|
||||
// Collapsing and flattening the collection doesn't work above.
|
||||
$acceptance = $unacceptedAssetGroup[0]['acceptance'];
|
||||
$locale = $acceptance->assignedTo?->locale;
|
||||
$email = $acceptance->assignedTo?->email;
|
||||
if(!$email){
|
||||
$this->info($acceptance->assignedTo->present()->fullName().' has no email address.');
|
||||
}
|
||||
$item_count = $unacceptedAssetGroup->count();
|
||||
foreach ($unacceptedAssetGroup as $unacceptedAsset) {
|
||||
// if ($unacceptedAsset['acceptance']->assignedTo->email == ''){
|
||||
// $no_mail_address[] = $unacceptedAsset['checkoutable']->assignedTo->present()->fullName;
|
||||
// }
|
||||
if ($unacceptedAsset['acceptance']->assignedTo) {
|
||||
|
||||
if (!$unacceptedAsset['acceptance']->assignedTo->locale) {
|
||||
Notification::locale(Setting::getSettings()->locale)->send(
|
||||
$unacceptedAsset['acceptance']->assignedTo,
|
||||
new UnacceptedAssetReminderNotification($unacceptedAsset['assetItem'], $count)
|
||||
);
|
||||
} else {
|
||||
Notification::send(
|
||||
$unacceptedAsset['acceptance']->assignedTo,
|
||||
new UnacceptedAssetReminderNotification($unacceptedAsset, $item_count)
|
||||
);
|
||||
}
|
||||
$count++;
|
||||
}
|
||||
if ($locale && $email) {
|
||||
Mail::to($email)->send((new UnacceptedAssetReminderMail($acceptance, $item_count))->locale($locale));
|
||||
|
||||
} elseif ($email) {
|
||||
Mail::to($email)->send((new UnacceptedAssetReminderMail($acceptance, $item_count)));
|
||||
}
|
||||
$count++;
|
||||
}
|
||||
|
||||
if (!empty($no_mail_address)) {
|
||||
foreach($no_mail_address as $user) {
|
||||
return $user.' has no email.';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
$this->info($count.' users notified.');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -765,9 +765,9 @@ class AssetsController extends Controller
|
||||
}
|
||||
|
||||
if ($problems_updating_encrypted_custom_fields) {
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.update.encrypted_warning')));
|
||||
return response()->json(Helper::formatStandardApiResponse('success', (new AssetsTransformer)->transformAsset($asset), trans('admin/hardware/message.update.encrypted_warning')));
|
||||
} else {
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.update.success')));
|
||||
return response()->json(Helper::formatStandardApiResponse('success', (new AssetsTransformer)->transformAsset($asset), trans('admin/hardware/message.update.success')));
|
||||
}
|
||||
}
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, $asset->getErrors()), 200);
|
||||
|
||||
@@ -283,6 +283,7 @@ class UsersController extends Controller
|
||||
'autoassign_licenses',
|
||||
'website',
|
||||
'locale',
|
||||
'notes',
|
||||
];
|
||||
|
||||
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'first_name';
|
||||
@@ -480,10 +481,11 @@ class UsersController extends Controller
|
||||
$user->permissions = $permissions_array;
|
||||
}
|
||||
|
||||
// Update the location of any assets checked out to this user
|
||||
Asset::where('assigned_type', User::class)
|
||||
->where('assigned_to', $user->id)->update(['location_id' => $request->input('location_id', null)]);
|
||||
|
||||
if($request->has('location_id')) {
|
||||
// Update the location of any assets checked out to this user
|
||||
Asset::where('assigned_type', User::class)
|
||||
->where('assigned_to', $user->id)->update(['location_id' => $request->input('location_id', null)]);
|
||||
}
|
||||
app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'image', 'avatars', 'avatar');
|
||||
|
||||
if ($user->save()) {
|
||||
|
||||
@@ -18,6 +18,7 @@ use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use \Illuminate\Contracts\View\View;
|
||||
use \Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\MessageBag;
|
||||
|
||||
|
||||
/**
|
||||
@@ -29,6 +30,7 @@ use \Illuminate\Http\RedirectResponse;
|
||||
*/
|
||||
class AssetModelsController extends Controller
|
||||
{
|
||||
protected MessageBag $validatorErrors;
|
||||
/**
|
||||
* Returns a view that invokes the ajax tables which actually contains
|
||||
* the content for the accessories listing, which is generated in getDatatable.
|
||||
@@ -158,7 +160,7 @@ class AssetModelsController extends Controller
|
||||
|
||||
if ($this->shouldAddDefaultValues($request->input())) {
|
||||
if (!$this->assignCustomFieldsDefaultValues($model, $request->input('default_values'))) {
|
||||
return redirect()->back()->withInput()->with('error', trans('admin/custom_fields/message.fieldset_default_value.error'));
|
||||
return redirect()->back()->withInput()->withErrors($this->validatorErrors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -481,9 +483,15 @@ class AssetModelsController extends Controller
|
||||
$rules[$field] = $validation;
|
||||
}
|
||||
|
||||
$validator = Validator::make($data, $rules);
|
||||
$attributes = [];
|
||||
foreach ($model->fieldset->fields as $field) {
|
||||
$attributes[$field->db_column] = trim(preg_replace('/_+|snipeit|\d+/', ' ', $field->db_column));
|
||||
}
|
||||
|
||||
$validator = Validator::make($data, $rules)->setAttributeNames($attributes);
|
||||
|
||||
if($validator->fails()){
|
||||
$this->validatorErrors = $validator->errors();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Helpers\Helper;
|
||||
use App\Mail\CheckoutAssetMail;
|
||||
use App\Models\Accessory;
|
||||
use App\Models\Actionlog;
|
||||
use App\Models\Asset;
|
||||
@@ -18,6 +19,7 @@ use App\Notifications\CheckoutAssetNotification;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use \Illuminate\Contracts\View\View;
|
||||
use League\Csv\Reader;
|
||||
@@ -1150,24 +1152,17 @@ class ReportsController extends Controller
|
||||
}
|
||||
$logItem = $logItem_res[0];
|
||||
}
|
||||
|
||||
$email = $assetItem->assignedTo?->email;
|
||||
$locale = $assetItem->assignedTo?->locale;
|
||||
// Only send notification if assigned
|
||||
if ($assetItem->assignedTo) {
|
||||
if ($locale && $email) {
|
||||
Mail::to($email)->send((new CheckoutAssetMail($assetItem, $assetItem->assignedTo, $logItem->user, $logItem->note, $acceptance))->locale($locale));
|
||||
|
||||
if (!$assetItem->assignedTo->locale) {
|
||||
Notification::locale(Setting::getSettings()->locale)->send(
|
||||
$assetItem->assignedTo,
|
||||
new CheckoutAssetNotification($assetItem, $assetItem->assignedTo, $logItem->user, $acceptance, $logItem->note)
|
||||
);
|
||||
} else {
|
||||
Notification::send(
|
||||
$assetItem->assignedTo,
|
||||
new CheckoutAssetNotification($assetItem, $assetItem->assignedTo, $logItem->user, $acceptance, $logItem->note)
|
||||
);
|
||||
} elseif ($email) {
|
||||
Mail::to($email)->send((new CheckoutAssetMail($assetItem, $assetItem->assignedTo, $logItem->user, $logItem->note, $acceptance)));
|
||||
}
|
||||
}
|
||||
|
||||
if ($assetItem->assignedTo->email == ''){
|
||||
if ($email == ''){
|
||||
return redirect()->route('reports/unaccepted_assets')->with('error', trans('general.no_email'));
|
||||
}
|
||||
|
||||
|
||||
@@ -334,6 +334,8 @@ class SettingsController extends Controller
|
||||
$setting->depreciation_method = $request->input('depreciation_method');
|
||||
$setting->dash_chart_type = $request->input('dash_chart_type');
|
||||
$setting->profile_edit = $request->input('profile_edit', 0);
|
||||
$setting->require_checkinout_notes = $request->input('require_checkinout_notes', 0);
|
||||
|
||||
|
||||
if ($request->input('per_page') != '') {
|
||||
$setting->per_page = $request->input('per_page');
|
||||
|
||||
@@ -21,9 +21,14 @@ class AssetCheckinRequest extends Request
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
$settings = \App\Models\Setting::getSettings();
|
||||
|
||||
];
|
||||
$rules = [];
|
||||
|
||||
if($settings->require_checkinout_notes) {
|
||||
$rules['note'] = 'string|required';
|
||||
}
|
||||
return $rules;
|
||||
}
|
||||
|
||||
public function response(array $errors)
|
||||
|
||||
@@ -21,6 +21,8 @@ class AssetCheckoutRequest extends Request
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
$settings = \App\Models\Setting::getSettings();
|
||||
|
||||
$rules = [
|
||||
'assigned_user' => 'required_without_all:assigned_asset,assigned_location',
|
||||
'assigned_asset' => 'required_without_all:assigned_user,assigned_location',
|
||||
@@ -35,7 +37,11 @@ class AssetCheckoutRequest extends Request
|
||||
'nullable',
|
||||
'date'
|
||||
],
|
||||
];
|
||||
];
|
||||
|
||||
if($settings->require_checkinout_notes) {
|
||||
$rules['note'] = 'required|string';
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use Illuminate\Contracts\Validation\Validator;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||
use App\Rules\UserCannotSwitchCompaniesIfItemsAssigned;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
class SaveUserRequest extends FormRequest
|
||||
{
|
||||
@@ -17,7 +18,7 @@ class SaveUserRequest extends FormRequest
|
||||
*/
|
||||
public function authorize()
|
||||
{
|
||||
return true;
|
||||
return (Gate::allows('users.create') || Gate::allows('users.edit'));
|
||||
}
|
||||
|
||||
public function response(array $errors)
|
||||
@@ -35,7 +36,8 @@ class SaveUserRequest extends FormRequest
|
||||
$rules = [
|
||||
'department_id' => 'nullable|exists:departments,id',
|
||||
'manager_id' => 'nullable|exists:users,id',
|
||||
'company_id' => ['nullable','exists:companies,id']
|
||||
'company_id' => ['nullable','exists:companies,id'],
|
||||
'groups' => ['nullable','exists:permission_groups,id']
|
||||
];
|
||||
|
||||
switch ($this->method()) {
|
||||
|
||||
@@ -100,7 +100,7 @@ class CheckoutableListener
|
||||
$notification = new TeamsNotification(Setting::getSettings()->webhook_endpoint);
|
||||
$notification->success()->sendMessage($message[0], $message[1]); // Send the message to Microsoft Teams
|
||||
} else {
|
||||
Notification::route(Setting::getSettings()->webhook_selected, Setting::getSettings()->webhook_endpoint)
|
||||
Notification::route($this->webhookSelected(), Setting::getSettings()->webhook_endpoint)
|
||||
->notify($this->getCheckoutNotification($event, $acceptance));
|
||||
}
|
||||
}
|
||||
@@ -182,7 +182,7 @@ class CheckoutableListener
|
||||
$notification = new TeamsNotification(Setting::getSettings()->webhook_endpoint);
|
||||
$notification->success()->sendMessage($message[0], $message[1]); // Send the message to Microsoft Teams
|
||||
} else {
|
||||
Notification::route(Setting::getSettings()->webhook_selected, Setting::getSettings()->webhook_endpoint)
|
||||
Notification::route($this->webhookSelected(), Setting::getSettings()->webhook_endpoint)
|
||||
->notify($this->getCheckinNotification($event));
|
||||
}
|
||||
}
|
||||
@@ -310,6 +310,13 @@ class CheckoutableListener
|
||||
return $event->checkedOutTo?->email ?? '';
|
||||
}
|
||||
}
|
||||
private function webhookSelected(){
|
||||
if(Setting::getSettings()->webhook_selected === 'slack' || Setting::getSettings()->webhook_selected === 'general'){
|
||||
return 'slack';
|
||||
}
|
||||
|
||||
return Setting::getSettings()->webhook_selected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the listeners for the subscriber.
|
||||
|
||||
@@ -24,7 +24,15 @@ class CustomFieldSetDefaultValuesForModel extends Component
|
||||
$this->fieldset_id = $this->model?->fieldset_id;
|
||||
$this->add_default_values = ($this->model?->defaultValues->count() > 0);
|
||||
|
||||
|
||||
$this->initializeSelectedValuesArray();
|
||||
if (session()->has('errors')) {
|
||||
$errors = session('errors')->keys();
|
||||
$selectedValuesKeys = array_keys($this->selectedValues);
|
||||
if (count(array_intersect($selectedValuesKeys, $errors)) > 0) {
|
||||
$this->add_default_values = true;
|
||||
};
|
||||
}
|
||||
$this->populatedSelectedValuesArray();
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class CheckinAccessoryMail extends Mailable
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
$from = new Address(config('mail.from.address'));
|
||||
$from = new Address(config('mail.from.address'), config('mail.from.name'));
|
||||
|
||||
return new Envelope(
|
||||
from: $from,
|
||||
|
||||
@@ -43,7 +43,7 @@ class CheckinAssetMail extends Mailable
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
$from = new Address(config('mail.from.address'));
|
||||
$from = new Address(config('mail.from.address'), config('mail.from.name'));
|
||||
|
||||
return new Envelope(
|
||||
from: $from,
|
||||
|
||||
@@ -34,7 +34,7 @@ class CheckinLicenseMail extends Mailable
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
$from = new Address(config('mail.from.address'));
|
||||
$from = new Address(config('mail.from.address'), config('mail.from.name'));
|
||||
|
||||
return new Envelope(
|
||||
from: $from,
|
||||
|
||||
@@ -37,7 +37,7 @@ class CheckoutAccessoryMail extends Mailable
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
$from = new Address(config('mail.from.address'));
|
||||
$from = new Address(config('mail.from.address'), config('mail.from.name'));
|
||||
|
||||
return new Envelope(
|
||||
from: $from,
|
||||
|
||||
@@ -52,7 +52,7 @@ class CheckoutAssetMail extends Mailable
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
$from = new Address(config('mail.from.address'));
|
||||
$from = new Address(config('mail.from.address'), config('mail.from.name'));
|
||||
|
||||
return new Envelope(
|
||||
from: $from,
|
||||
|
||||
@@ -38,7 +38,7 @@ class CheckoutConsumableMail extends Mailable
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
$from = new Address(config('mail.from.address'));
|
||||
$from = new Address(config('mail.from.address'), config('mail.from.name'));
|
||||
|
||||
return new Envelope(
|
||||
from: $from,
|
||||
|
||||
@@ -36,7 +36,7 @@ class CheckoutLicenseMail extends Mailable
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
$from = new Address(config('mail.from.address'));
|
||||
$from = new Address(config('mail.from.address'), config('mail.from.name'));
|
||||
|
||||
return new Envelope(
|
||||
from: $from,
|
||||
|
||||
67
app/Mail/UnacceptedAssetReminderMail.php
Normal file
67
app/Mail/UnacceptedAssetReminderMail.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Address;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class UnacceptedAssetReminderMail extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*/
|
||||
public function __construct($checkout_info, $count)
|
||||
{
|
||||
$this->count = $count;
|
||||
$this->target = $checkout_info['acceptance']?->assignedTo;
|
||||
$this->acceptance = $checkout_info['acceptance'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
$from = new Address(config('mail.from.address'), config('mail.from.name'));
|
||||
|
||||
return new Envelope(
|
||||
from: $from,
|
||||
subject: trans('mail.unaccepted_asset_reminder'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message content definition.
|
||||
*/
|
||||
public function content(): Content
|
||||
{
|
||||
$accept_url = route('account.accept');
|
||||
|
||||
return new Content(
|
||||
markdown: 'notifications.markdown.asset-reminder',
|
||||
with: [
|
||||
'count' => $this->count,
|
||||
'assigned_to' => $this->target?->present()->fullName,
|
||||
'link' => route('account.accept'),
|
||||
'accept_url' => $accept_url,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachments for the message.
|
||||
*
|
||||
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
|
||||
*/
|
||||
public function attachments(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,7 @@ class Setting extends Model
|
||||
|
||||
protected $casts = [
|
||||
'label2_asset_logo' => 'boolean',
|
||||
'require_checkinout_notes' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\Asset;
|
||||
use App\Models\User;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class UnacceptedAssetReminderNotification extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
/**
|
||||
* Create a new notification instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($checkout_info, $count)
|
||||
{
|
||||
$this->count = $count;
|
||||
$this->target = $checkout_info['acceptance']->assignedTo;
|
||||
$this->acceptance = $checkout_info['acceptance'];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return array
|
||||
*/
|
||||
public function via()
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return \Illuminate\Notifications\Messages\MailMessage
|
||||
*/
|
||||
public function toMail()
|
||||
{
|
||||
$accept_url = route('account.accept');
|
||||
$message = (new MailMessage)->markdown('notifications.markdown.asset-reminder',
|
||||
[
|
||||
'count' => $this->count,
|
||||
'assigned_to' => $this->target->present()->fullName,
|
||||
'link' => route('account.accept'),
|
||||
'accept_url' => $accept_url,
|
||||
])
|
||||
->subject(trans('mail.unaccepted_asset_reminder'));
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return array
|
||||
*/
|
||||
public function toArray($notifiable)
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
abstract class SnipePermissionsPolicy
|
||||
{
|
||||
/**
|
||||
* This should return the key of the model in the users json permission string.
|
||||
* This should return the key of the model in the user's JSON permission string.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
@@ -37,11 +37,7 @@ abstract class SnipePermissionsPolicy
|
||||
{
|
||||
/**
|
||||
* If an admin, they can do all item related tasks, but ARE constrained by FMCSA company access.
|
||||
* That scoping happens on the model level (except for the Users model) via the Companyable trait.
|
||||
*
|
||||
* This does lead to some inconsistencies in the responses, since attempting to edit assets,
|
||||
* accessories, etc (anything other than users) will result in a Forbidden error, whereas the users
|
||||
* area will redirect with "That user doesn't exist" since the scoping is handled directly on those queries.
|
||||
* That scoping happens on the model level via the Companyable trait.
|
||||
*
|
||||
* The *superuser* global permission gets handled in the AuthServiceProvider before() method.
|
||||
*
|
||||
@@ -53,7 +49,7 @@ abstract class SnipePermissionsPolicy
|
||||
}
|
||||
|
||||
/**
|
||||
* If we got here by $this→authorize('something', $actualModel) then we can continue on Il but if we got here
|
||||
* If we got here by $this→authorize('something', $actualModel) then we can continue on, but if we got here
|
||||
* via $this→authorize('something', Model::class) then calling Company:: isCurrentUserHasAccess($item) gets weird.
|
||||
* Bail out here by returning "nothing" and allow the relevant method lower in this class to be called and handle authorization.
|
||||
*/
|
||||
@@ -85,7 +81,7 @@ abstract class SnipePermissionsPolicy
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view the accessory.
|
||||
* Determine whether the user can view the item.
|
||||
*
|
||||
* @param \App\Models\User $user
|
||||
* @return mixed
|
||||
@@ -112,7 +108,7 @@ abstract class SnipePermissionsPolicy
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the accessory.
|
||||
* Determine whether the user can update the item.
|
||||
*
|
||||
* @param \App\Models\User $user
|
||||
* @return mixed
|
||||
@@ -124,7 +120,7 @@ abstract class SnipePermissionsPolicy
|
||||
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the accessory.
|
||||
* Determine whether the user can checkout the item.
|
||||
*
|
||||
* @param \App\Models\User $user
|
||||
* @return mixed
|
||||
@@ -135,7 +131,7 @@ abstract class SnipePermissionsPolicy
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete the accessory.
|
||||
* Determine whether the user can delete the item.
|
||||
*
|
||||
* @param \App\Models\User $user
|
||||
* @return mixed
|
||||
@@ -151,7 +147,7 @@ abstract class SnipePermissionsPolicy
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can manage the accessory.
|
||||
* Determine whether the user can manage the item.
|
||||
*
|
||||
* @param \App\Models\User $user
|
||||
* @return mixed
|
||||
|
||||
@@ -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('settings', function (Blueprint $table) {
|
||||
$table->boolean('require_checkinout_notes')->nullable()->default(0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('settings', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('settings', 'require_checkinout_notes')) {
|
||||
$table->dropColumn('require_checkinout_notes');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -21,6 +21,7 @@
|
||||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||
<env name="CACHE_DRIVER" value="array"/>
|
||||
<env name="MAIL_FROM_ADDR" value="app@example.com"/>
|
||||
<env name="MAIL_MAILER" value="array"/>
|
||||
<env name="QUEUE_DRIVER" value="sync"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
public/css/dist/all.css
vendored
2
public/css/dist/all.css
vendored
File diff suppressed because one or more lines are too long
@@ -2,8 +2,8 @@
|
||||
"/js/build/app.js": "/js/build/app.js?id=5572f3bd32a6131651ab3022edd76941",
|
||||
"/css/dist/skins/skin-black-dark.css": "/css/dist/skins/skin-black-dark.css?id=d34ae2483cbe2c77478c45f4006eba55",
|
||||
"/css/dist/skins/_all-skins.css": "/css/dist/skins/_all-skins.css?id=b1c78591f51b52beab05b52f407ad6e6",
|
||||
"/css/build/overrides.css": "/css/build/overrides.css?id=49a09bf67bd6129ef4ce910d0be09757",
|
||||
"/css/build/app.css": "/css/build/app.css?id=6b9acfefe41b964aa29d58dd26d6f910",
|
||||
"/css/build/overrides.css": "/css/build/overrides.css?id=b1146d30456ed95c3d4a9b60ddc58313",
|
||||
"/css/build/app.css": "/css/build/app.css?id=e5f692af9dd2c217455ad87e520ef32a",
|
||||
"/css/build/AdminLTE.css": "/css/build/AdminLTE.css?id=a67bd93bed52e6a29967fe472de66d6c",
|
||||
"/css/dist/skins/skin-yellow.css": "/css/dist/skins/skin-yellow.css?id=fc7adb943668ac69fe4b646625a7571f",
|
||||
"/css/dist/skins/skin-yellow-dark.css": "/css/dist/skins/skin-yellow-dark.css?id=53edc92eb2d272744bc7404ec259930e",
|
||||
@@ -19,7 +19,7 @@
|
||||
"/css/dist/skins/skin-blue.css": "/css/dist/skins/skin-blue.css?id=392cc93cfc0be0349bab9697669dd091",
|
||||
"/css/dist/skins/skin-blue-dark.css": "/css/dist/skins/skin-blue-dark.css?id=18787b3f00a3be7be38ee4e26cbd2a07",
|
||||
"/css/dist/skins/skin-black.css": "/css/dist/skins/skin-black.css?id=1f33ca3d860461c1127ec465ab3ebb6b",
|
||||
"/css/dist/all.css": "/css/dist/all.css?id=9a85a7206ecd9a5e74ec0736bec0026e",
|
||||
"/css/dist/all.css": "/css/dist/all.css?id=fa28de72acfa72bfdd7ad4206a65d4eb",
|
||||
"/css/dist/signature-pad.css": "/css/dist/signature-pad.css?id=6a89d3cd901305e66ced1cf5f13147f7",
|
||||
"/css/dist/signature-pad.min.css": "/css/dist/signature-pad.min.css?id=6a89d3cd901305e66ced1cf5f13147f7",
|
||||
"/js/select2/i18n/af.js": "/js/select2/i18n/af.js?id=4f6fcd73488ce79fae1b7a90aceaecde",
|
||||
|
||||
@@ -384,7 +384,7 @@ a.logo.no-hover a:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
input:required, select:required {
|
||||
input:required, select:required, textarea:required {
|
||||
border-right: 6px solid orange;
|
||||
}
|
||||
|
||||
|
||||
@@ -876,7 +876,6 @@ th.css-component > .th-inner::before
|
||||
}
|
||||
@media screen and (max-width: 992px){
|
||||
.info-stack-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.col-md-3.col-xs-12.col-sm-push-9.info-stack{
|
||||
@@ -892,6 +891,12 @@ th.css-component > .th-inner::before
|
||||
float:none;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 992px){
|
||||
.row-new-striped div{
|
||||
width:100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1318px) and (min-width: 1200px){
|
||||
.admin.box{
|
||||
height:170px;
|
||||
|
||||
@@ -280,6 +280,8 @@ return [
|
||||
'two_factor_enrollment_text' => "Two factor authentication is required, however your device has not been enrolled yet. Open your Google Authenticator app and scan the QR code below to enroll your device. Once you've enrolled your device, enter the code below",
|
||||
'require_accept_signature' => 'Require Signature',
|
||||
'require_accept_signature_help_text' => 'Enabling this feature will require users to physically sign off on accepting an asset.',
|
||||
'require_checkinout_notes' => 'Require Notes on Checkin/Checkout',
|
||||
'require_checkinout_notes_help_text' => 'Enabling this feature will require the note fields to be populated when checking in or checking out an asset.',
|
||||
'left' => 'left',
|
||||
'right' => 'right',
|
||||
'top' => 'top',
|
||||
|
||||
@@ -113,17 +113,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Note -->
|
||||
<div class="form-group {{ $errors->has('note') ? 'error' : '' }}">
|
||||
<label for="note" class="col-sm-3 control-label">
|
||||
{{ trans('general.notes') }}
|
||||
</label>
|
||||
<div class="col-md-8">
|
||||
<textarea class="col-md-6 form-control" id="note"
|
||||
name="note">{{ old('note', $asset->note) }}</textarea>
|
||||
{!! $errors->first('note', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Note -->
|
||||
<div class="form-group {{ $errors->has('note') ? 'error' : '' }}">
|
||||
<label for="note" class="col-md-3 control-label">
|
||||
{{ trans('general.notes') }}
|
||||
</label>
|
||||
<div class="col-md-8">
|
||||
<textarea class="col-md-6 form-control" id="note" @required($snipeSettings->require_checkinout_notes)
|
||||
name="note">{{ old('note', $asset->note) }}</textarea>
|
||||
{!! $errors->first('note', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
|
||||
</div>
|
||||
</div>
|
||||
</div> <!--/.box-body-->
|
||||
</div> <!--/.box-body-->
|
||||
|
||||
|
||||
@@ -141,8 +141,9 @@
|
||||
<label for="note" class="col-md-3 control-label">
|
||||
{{ trans('general.notes') }}
|
||||
</label>
|
||||
|
||||
<div class="col-md-8">
|
||||
<textarea class="col-md-6 form-control" id="note"
|
||||
<textarea class="col-md-6 form-control" id="note" @required($snipeSettings->require_checkinout_notes)
|
||||
name="note">{{ old('note', $asset->note) }}</textarea>
|
||||
{!! $errors->first('note', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<span>
|
||||
|
||||
<div class="form-group{{ $errors->has('custom_fieldset') ? ' has-error' : '' }}">
|
||||
<label for="custom_fieldset" class="col-md-3 control-label">
|
||||
{{ trans('admin/models/general.fieldset') }}
|
||||
@@ -11,6 +10,7 @@
|
||||
<div class="col-md-3">
|
||||
@if ($fieldset_id)
|
||||
<label class="form-control">
|
||||
|
||||
{{ Form::checkbox('add_default_values', 1, old('add_default_values', $add_default_values), ['data-livewire-component' => $this->getId(), 'id' => 'add_default_values', 'wire:model.live' => 'add_default_values', 'disabled' => $this->fields->isEmpty()]) }}
|
||||
{{ trans('admin/models/general.add_default_values') }}
|
||||
</label>
|
||||
@@ -19,12 +19,13 @@
|
||||
</div>
|
||||
|
||||
@if ($add_default_values)
|
||||
@if ($this->fields)
|
||||
|
||||
@if ($this->fields)
|
||||
|
||||
@foreach ($this->fields as $field)
|
||||
<div class="form-group" wire:key="field-{{ $field->id }}">
|
||||
|
||||
<label class="col-md-3 control-label{{ $errors->has($field->name) ? ' has-error' : '' }}">{{ $field->name }}</label>
|
||||
<label class="col-md-3 control-label{{ $errors->has($field->db_column_name()) ? ' has-error' : '' }}">{{ $field->name }}</label>
|
||||
|
||||
<div class="col-md-7">
|
||||
|
||||
@@ -123,10 +124,16 @@
|
||||
Unknown field element: {{ $field->element }}
|
||||
</span>
|
||||
@endif
|
||||
<?php
|
||||
$errormessage = $errors->first($field->db_column_name());
|
||||
if ($errormessage) {
|
||||
print('<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> '.$errormessage.'</span>');
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endforeach
|
||||
@endforeach
|
||||
|
||||
@endif
|
||||
|
||||
|
||||
@@ -137,14 +137,16 @@
|
||||
</button>
|
||||
<a href="#" wire:click.prevent="$set('activeFileId',null)">
|
||||
<button class="btn btn-sm btn-danger" wire:click="destroy({{ $currentFile->id }})">
|
||||
<i class="fas fa-trash icon-white" aria-hidden="true"></i><span class="sr-only"></span></button>
|
||||
<i class="fas fa-trash icon-white" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{ trans('general.delete') }}</span>
|
||||
</button>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@if( $currentFile && $this->activeFile && ($currentFile->id == $this->activeFile->id))
|
||||
<tr class="warning">
|
||||
<td colspan="4">
|
||||
<td colspan="5">
|
||||
|
||||
<div class="form-group">
|
||||
|
||||
|
||||
@@ -2,20 +2,22 @@
|
||||
@foreach($model->fieldset->fields AS $field)
|
||||
<div class="form-group{{ $errors->has($field->db_column_name()) ? ' has-error' : '' }}">
|
||||
<label for="{{ $field->db_column_name() }}" class="col-md-3 control-label">{{ $field->name }} </label>
|
||||
<div class="col-md-7 col-sm-12{{ ($field->pivot->required=='1') ? ' required' : '' }}">
|
||||
<div class="col-md-7 col-sm-12">
|
||||
|
||||
|
||||
@if ($field->element!='text')
|
||||
<!-- Listbox -->
|
||||
|
||||
@if ($field->element=='listbox')
|
||||
<!-- Listbox -->
|
||||
{{ Form::select($field->db_column_name(), $field->formatFieldValuesAsArray(),
|
||||
old($field->db_column_name(),(isset($item) ? Helper::gracefulDecrypt($field, $item->{$field->db_column_name()}) : $field->defaultValue($model->id))), ['class'=>'format select2 form-control']) }}
|
||||
old($field->db_column_name(),(isset($item) ? Helper::gracefulDecrypt($field, $item->{$field->db_column_name()}) : $field->defaultValue($model->id))), ['class' => 'format select2 form-control', ($field->pivot->required=='1' ? ' required' : '') ]) }}
|
||||
|
||||
@elseif ($field->element=='textarea')
|
||||
<textarea class="col-md-6 form-control" id="{{ $field->db_column_name() }}" name="{{ $field->db_column_name() }}">{{ old($field->db_column_name(),(isset($item) ? Helper::gracefulDecrypt($field, $item->{$field->db_column_name()}) : $field->defaultValue($model->id))) }}</textarea>
|
||||
<!-- Textarea -->
|
||||
<textarea class="col-md-6 form-control" id="{{ $field->db_column_name() }}" name="{{ $field->db_column_name() }}"{{ ($field->pivot->required=='1') ? ' required' : '' }}>{{ old($field->db_column_name(),(isset($item) ? Helper::gracefulDecrypt($field, $item->{$field->db_column_name()}) : $field->defaultValue($model->id))) }}</textarea>
|
||||
|
||||
@elseif ($field->element=='checkbox')
|
||||
<!-- Checkboxes -->
|
||||
<!-- Checkbox -->
|
||||
@foreach ($field->formatFieldValuesAsArray() as $key => $value)
|
||||
<div>
|
||||
<label class="form-control">
|
||||
@@ -26,27 +28,25 @@
|
||||
@endforeach
|
||||
|
||||
@elseif ($field->element=='radio')
|
||||
@foreach ($field->formatFieldValuesAsArray() as $value)
|
||||
|
||||
<div>
|
||||
<label class="form-control">
|
||||
<input type="radio" value="{{ $value }}" name="{{ $field->db_column_name() }}" {{ isset($item) ? ($item->{$field->db_column_name()} == $value ? ' checked="checked"' : '') : (old($field->db_column_name()) != '' ? ' checked="checked"' : (in_array($value, explode(', ', $field->defaultValue($model->id))) ? ' checked="checked"' : '')) }}>
|
||||
{{ $value }}
|
||||
</label>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
<!-- Radio -->
|
||||
@foreach ($field->formatFieldValuesAsArray() as $value)
|
||||
<div>
|
||||
<label class="form-control">
|
||||
<input type="radio" value="{{ $value }}" name="{{ $field->db_column_name() }}" {{ isset($item) ? ($item->{$field->db_column_name()} == $value ? ' checked="checked"' : '') : (old($field->db_column_name()) != '' ? ' checked="checked"' : (in_array($value, explode(', ', $field->defaultValue($model->id))) ? ' checked="checked"' : '')) }}>
|
||||
{{ $value }}
|
||||
</label>
|
||||
</div>
|
||||
@endforeach
|
||||
@endif
|
||||
|
||||
|
||||
@else
|
||||
<!-- Date field -->
|
||||
|
||||
@if ($field->format=='DATE')
|
||||
|
||||
<div class="input-group col-md-5" style="padding-left: 0px;">
|
||||
<div class="input-group date" data-provide="datepicker" data-date-format="yyyy-mm-dd" data-autoclose="true" data-date-clear-btn="true">
|
||||
<input type="text" class="form-control" placeholder="{{ trans('general.select_date') }}" name="{{ $field->db_column_name() }}" id="{{ $field->db_column_name() }}" readonly value="{{ old($field->db_column_name(),(isset($item) ? Helper::gracefulDecrypt($field, $item->{$field->db_column_name()}) : $field->defaultValue($model->id))) }}" style="background-color:inherit">
|
||||
<input type="text" class="form-control" placeholder="{{ trans('general.select_date') }}" name="{{ $field->db_column_name() }}" id="{{ $field->db_column_name() }}" readonly value="{{ old($field->db_column_name(),(isset($item) ? Helper::gracefulDecrypt($field, $item->{$field->db_column_name()}) : $field->defaultValue($model->id))) }}" style="background-color:inherit"{{ ($field->pivot->required=='1') ? ' required' : '' }}>
|
||||
<span class="input-group-addon"><x-icon type="calendar" /></span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,7 +54,7 @@
|
||||
|
||||
@else
|
||||
@if (($field->field_encrypted=='0') || (Gate::allows('assets.view.encrypted_custom_fields')))
|
||||
<input type="text" value="{{ old($field->db_column_name(),(isset($item) ? Helper::gracefulDecrypt($field, $item->{$field->db_column_name()}) : $field->defaultValue($model->id))) }}" id="{{ $field->db_column_name() }}" class="form-control" name="{{ $field->db_column_name() }}" placeholder="Enter {{ strtolower($field->format) }} text">
|
||||
<input type="text" value="{{ old($field->db_column_name(),(isset($item) ? Helper::gracefulDecrypt($field, $item->{$field->db_column_name()}) : $field->defaultValue($model->id))) }}" id="{{ $field->db_column_name() }}" class="form-control" name="{{ $field->db_column_name() }}" placeholder="Enter {{ strtolower($field->format) }} text"{{ ($field->pivot->required=='1') ? ' required' : '' }}>
|
||||
@else
|
||||
<input type="text" value="{{ strtoupper(trans('admin/custom_fields/general.encrypted')) }}" class="form-control disabled" disabled>
|
||||
@endif
|
||||
@@ -66,13 +66,13 @@
|
||||
<p class="help-block">{{ $field->help_text }}</p>
|
||||
@endif
|
||||
|
||||
<?php
|
||||
$errormessage=$errors->first($field->db_column_name());
|
||||
if ($errormessage) {
|
||||
$errormessage=preg_replace('/ snipeit /', '', $errormessage);
|
||||
print('<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> '.$errormessage.'</span>');
|
||||
}
|
||||
?>
|
||||
<?php
|
||||
$errormessage = $errors->first($field->db_column_name());
|
||||
if ($errormessage) {
|
||||
$errormessage = preg_replace('/ snipeit /', '', $errormessage);
|
||||
print('<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> '.$errormessage.'</span>');
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
|
||||
@if ($field->field_encrypted)
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
<!-- Custom Fieldset -->
|
||||
<!-- If $item->id is null we are cloning the model and we need the $model_id variable -->
|
||||
@livewire('custom-field-set-default-values-for-model',["model_id" => $item->id ?? $model_id ?? null ])
|
||||
@livewire('custom-field-set-default-values-for-model', ["model_id" => $item->id ?? $model_id ?? null])
|
||||
|
||||
@include ('partials.forms.edit.notes')
|
||||
@include ('partials.forms.edit.requestable', ['requestable_text' => trans('admin/models/general.requestable')])
|
||||
|
||||
@@ -9,12 +9,15 @@
|
||||
## {{ $assets->count() }} {{ trans('general.assets') }}
|
||||
|
||||
<table width="100%">
|
||||
<tr><th align="left">{{ trans('mail.name') }} </th><th align="left">{{ trans('mail.asset_tag') }}</th><th align="left">{{ trans('admin/hardware/table.serial') }}</th> <th></th> </tr>
|
||||
<tr><th align="left">{{ trans('mail.name') }} </th><th align="left">{{ trans('mail.asset_tag') }}</th><th align="left">{{ trans('admin/hardware/table.serial') }}</th><th align="left">{{ trans('general.category') }}</th> <th></th> </tr>
|
||||
|
||||
|
||||
@foreach($assets as $asset)
|
||||
<tr>
|
||||
<td>{{ $asset->present()->name }}</td>
|
||||
<td> {{ $asset->asset_tag }} </td>
|
||||
<td> {{ $asset->serial }} </td>
|
||||
<td> {{ $asset->model->category->name }}</td>
|
||||
@if (($snipeSettings->show_images_in_email =='1') && $asset->getImageUrl())
|
||||
<td>
|
||||
<img src="{{ asset($asset->getImageUrl()) }}" alt="Asset" style="max-width: 64px;">
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
@can('view', \App\Models\User::class)
|
||||
<div id="userBulkEditToolbar">
|
||||
{{ Form::open([
|
||||
<div id="userBulkEditToolbar" class="pull-left" style="min-width:500px !important; padding-top: 10px;">
|
||||
|
||||
@if (request('status')!='deleted')
|
||||
|
||||
{{ Form::open([
|
||||
'method' => 'POST',
|
||||
'route' => ['users/bulkedit'],
|
||||
'class' => 'form-inline',
|
||||
'id' => 'usersBulkForm']) }}
|
||||
|
||||
@if (request('status')!='deleted')
|
||||
<div id="users-toolbar">
|
||||
|
||||
<div id="users-toolbar" style="width:100% !important;">
|
||||
<label for="bulk_actions" class="sr-only">{{ trans('general.bulk_actions') }}</label>
|
||||
<select name="bulk_actions" class="form-control select2" style="min-width:300px;" aria-label="bulk_actions">
|
||||
<select name="bulk_actions" class="form-control select2" style="width: 50% !important;" aria-label="bulk_actions">
|
||||
|
||||
@can('update', \App\Models\User::class)
|
||||
<option value="edit">{{ trans('general.bulk_edit') }}</option>
|
||||
@@ -25,7 +28,8 @@
|
||||
</select>
|
||||
<button class="btn btn-primary" id="bulkUserEditButton" disabled>{{ trans('button.go') }}</button>
|
||||
</div>
|
||||
{{ Form::close() }}
|
||||
@endif
|
||||
{{ Form::close() }}
|
||||
|
||||
</div>
|
||||
@endcan
|
||||
|
||||
@@ -215,6 +215,23 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Require Notes on checkin/checkout checkbox -->
|
||||
<div class="form-group">
|
||||
<div class="col-md-3">
|
||||
<label>
|
||||
{{ trans('admin/settings/general.require_checkinout_notes') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<label class="form-control">
|
||||
<input type="checkbox" value="1" name="require_checkinout_notes" {{ (old('require_checkinout_notes', $setting->require_checkinout_notes)) == '1' ? ' checked="checked"' : '' }} aria-label="require_checkinout_notes">
|
||||
{{ trans('general.yes') }}
|
||||
</label>
|
||||
<p class="help-block">{{ trans('admin/settings/general.require_checkinout_notes_help_text') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.form-group -->
|
||||
|
||||
|
||||
<!-- login text -->
|
||||
<div class="form-group {{ $errors->has('login_note') ? 'error' : '' }}">
|
||||
|
||||
@@ -309,10 +309,10 @@
|
||||
<div class="row">
|
||||
<!-- name -->
|
||||
|
||||
<div class="col-md-3 col-sm-2">
|
||||
<div class="col-md-3">
|
||||
{{ trans('admin/users/table.name') }}
|
||||
</div>
|
||||
<div class="col-md-9 col-sm-2">
|
||||
<div class="col-md-9">
|
||||
{{ $user->present()->fullName() }}
|
||||
</div>
|
||||
|
||||
@@ -751,7 +751,7 @@
|
||||
{{Helper::formatCurrencyOutput($user->getUserTotalCost()->total_user_cost)}}
|
||||
|
||||
<a id="optional_info" class="text-primary">
|
||||
<x-icon type="caret-right" id="optional_info_icon" /></i>
|
||||
<x-icon type="caret-right" id="optional_info_icon" />
|
||||
<strong>{{ trans('admin/hardware/form.optional_infos') }}</strong>
|
||||
</a>
|
||||
</div>
|
||||
@@ -1064,7 +1064,7 @@
|
||||
|
||||
<div class="tab-pane" id="managed-users">
|
||||
|
||||
@include('partials.locations-bulk-actions')
|
||||
@include('partials.users-bulk-actions')
|
||||
|
||||
|
||||
<table
|
||||
@@ -1074,7 +1074,7 @@
|
||||
data-pagination="true"
|
||||
data-id-table="managedUsersTable"
|
||||
data-toolbar="#usersBulkEditToolbar"
|
||||
data-bulk-button-id="#bulkUsersEditButton"
|
||||
data-bulk-button-id="#bulkUserEditButton"
|
||||
data-bulk-form-id="#usersBulkForm"
|
||||
data-search="true"
|
||||
data-show-footer="true"
|
||||
|
||||
53
tests/Feature/Console/SendAcceptanceReminderTest.php
Normal file
53
tests/Feature/Console/SendAcceptanceReminderTest.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Console;
|
||||
|
||||
use App\Mail\UnacceptedAssetReminderMail;
|
||||
use App\Models\CheckoutAcceptance;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Tests\TestCase;
|
||||
class SendAcceptanceReminderTest extends TestCase
|
||||
{
|
||||
public function testAcceptanceReminderCommand()
|
||||
{
|
||||
Mail::fake();
|
||||
$userA = User::factory()->create(['email' => 'userA@test.com']);
|
||||
$userB = User::factory()->create(['email' => 'userB@test.com']);
|
||||
|
||||
CheckoutAcceptance::factory()->pending()->count(2)->create([
|
||||
'assigned_to_id' => $userA->id,
|
||||
]);
|
||||
CheckoutAcceptance::factory()->pending()->create([
|
||||
'assigned_to_id' => $userB->id,
|
||||
]);
|
||||
|
||||
$this->artisan('snipeit:acceptance-reminder')->assertExitCode(0);
|
||||
|
||||
Mail::assertSent(UnacceptedAssetReminderMail::class, function ($mail) {
|
||||
return $mail->hasTo('userA@test.com');
|
||||
});
|
||||
|
||||
Mail::assertSent(UnacceptedAssetReminderMail::class, function ($mail) {
|
||||
return $mail->hasTo('userB@test.com');
|
||||
});
|
||||
|
||||
Mail::assertSent(UnacceptedAssetReminderMail::class,2);
|
||||
}
|
||||
|
||||
public function testAcceptanceReminderCommandHandlesUserWithoutEmail()
|
||||
{
|
||||
Mail::fake();
|
||||
$userA = User::factory()->create(['email' => '']);
|
||||
|
||||
CheckoutAcceptance::factory()->pending()->create([
|
||||
'assigned_to_id' => $userA->id,
|
||||
]);
|
||||
|
||||
$this->artisan('snipeit:acceptance-reminder')
|
||||
->expectsOutput($userA->present()->fullName().' has no email address.')
|
||||
->assertExitCode(0);
|
||||
|
||||
Mail::assertNotSent(UnacceptedAssetReminderMail::class);
|
||||
}
|
||||
}
|
||||
91
tests/Feature/Users/Api/StoreUserTest.php
Normal file
91
tests/Feature/Users/Api/StoreUserTest.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Users\Api;
|
||||
|
||||
use App\Models\Company;
|
||||
use App\Models\Department;
|
||||
use App\Models\Group;
|
||||
use App\Models\Location;
|
||||
use App\Models\User;
|
||||
use Tests\TestCase;
|
||||
|
||||
class StoreUserTest extends TestCase {
|
||||
public function testRequiresPermission()
|
||||
{
|
||||
|
||||
$request = $this->actingAsForApi(User::factory()->create())
|
||||
->postJson(route('api.users.store'))
|
||||
->assertForbidden()
|
||||
->json();
|
||||
}
|
||||
|
||||
public function testCanSaveUserViaPost()
|
||||
{
|
||||
$admin = User::factory()->superuser()->create();
|
||||
$manager = User::factory()->create();
|
||||
$company = Company::factory()->create();
|
||||
$department = Department::factory()->create();
|
||||
$location = Location::factory()->create();
|
||||
$group = Group::factory()->create();
|
||||
|
||||
|
||||
$response = $this->actingAsForApi($admin)
|
||||
->postJson(route('api.users.store'), [
|
||||
'first_name' => 'Mabel',
|
||||
'last_name' => 'Mora',
|
||||
'username' => 'mabel',
|
||||
'password' => 'super-secret',
|
||||
'password_confirmation' => 'super-secret',
|
||||
'email' => 'mabel@example.com',
|
||||
'permissions' => '{"a.new.permission":"1"}',
|
||||
'activated' => true,
|
||||
'phone' => '619-555-5555',
|
||||
'jobtitle' => 'Host',
|
||||
'manager_id' => $manager->id,
|
||||
'employee_num' => '1111',
|
||||
'notes' => 'Pretty good artist',
|
||||
'company_id' => $company->id,
|
||||
'department_id' => $department->id,
|
||||
'location_id' => $location->id,
|
||||
'remote' => true,
|
||||
'groups' => $group->id,
|
||||
'vip' => true,
|
||||
'start_date' => '2021-08-01',
|
||||
'end_date' => '2025-12-31',
|
||||
])
|
||||
->assertOk()
|
||||
->assertStatus(200)
|
||||
->assertStatusMessageIs('success');
|
||||
|
||||
$user = User::find($response['payload']['id']);
|
||||
$this->assertEquals($admin->id, $user->created_by, 'Created by was not saved');
|
||||
}
|
||||
|
||||
public function testDoesNotAcceptBogusGroupData()
|
||||
{
|
||||
$admin = User::factory()->superuser()->create();
|
||||
|
||||
$this->actingAsForApi($admin)
|
||||
->postJson(route('api.users.store'), [
|
||||
'first_name' => 'Mabel',
|
||||
'username' => 'mabel',
|
||||
'password' => 'super-secret',
|
||||
'password_confirmation' => 'super-secret',
|
||||
'groups' => ['blah'],
|
||||
])
|
||||
->assertOk()
|
||||
->assertStatus(200)
|
||||
->assertStatusMessageIs('error')
|
||||
->assertJson(
|
||||
[
|
||||
'messages' => [
|
||||
'groups' =>
|
||||
[0 => trans('The selected groups is invalid.')]
|
||||
]
|
||||
])
|
||||
->json();
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user