Merge remote-tracking branch 'origin/develop'

Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	public/css/build/app.css
#	public/css/build/overrides.css
#	public/css/dist/all.css
#	public/mix-manifest.json
This commit is contained in:
snipe 2024-08-22 11:20:42 +01:00
commit 1b310f3a24
18 changed files with 434 additions and 34 deletions

View file

@ -3190,6 +3190,15 @@
"contributions": [
"code"
]
},
{
"login": "Scarzy",
"name": "Scarzy",
"avatar_url": "https://avatars.githubusercontent.com/u/1197791?v=4",
"profile": "https://github.com/Scarzy",
"contributions": [
"code"
]
}
]
}

View file

@ -52,7 +52,7 @@ Thanks goes to all of these wonderful people ([emoji key](https://github.com/ken
| [<img src="https://avatars.githubusercontent.com/u/47315739?v=4" width="110px;"/><br /><sub>bilias</sub>](https://github.com/bilias)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bilias "Code") | [<img src="https://avatars.githubusercontent.com/u/2565989?v=4" width="110px;"/><br /><sub>coach1988</sub>](https://github.com/coach1988)<br />[💻](https://github.com/snipe/snipe-it/commits?author=coach1988 "Code") | [<img src="https://avatars.githubusercontent.com/u/11910225?v=4" width="110px;"/><br /><sub>MrM</sub>](https://github.com/mauro-miatello)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mauro-miatello "Code") | [<img src="https://avatars.githubusercontent.com/u/60405354?v=4" width="110px;"/><br /><sub>koiakoia</sub>](https://github.com/koiakoia)<br />[💻](https://github.com/snipe/snipe-it/commits?author=koiakoia "Code") | [<img src="https://avatars.githubusercontent.com/u/5323832?v=4" width="110px;"/><br /><sub>Mustafa Online</sub>](https://github.com/mustafa-online)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mustafa-online "Code") | [<img src="https://avatars.githubusercontent.com/u/104601439?v=4" width="110px;"/><br /><sub>franceslui</sub>](https://github.com/franceslui)<br />[💻](https://github.com/snipe/snipe-it/commits?author=franceslui "Code") | [<img src="https://avatars.githubusercontent.com/u/125313163?v=4" width="110px;"/><br /><sub>Q4kK</sub>](https://github.com/Q4kK)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Q4kK "Code") |
| [<img src="https://avatars.githubusercontent.com/u/55590532?v=4" width="110px;"/><br /><sub>squintfox</sub>](https://github.com/squintfox)<br />[💻](https://github.com/snipe/snipe-it/commits?author=squintfox "Code") | [<img src="https://avatars.githubusercontent.com/u/1380084?v=4" width="110px;"/><br /><sub>Jeff Clay</sub>](https://github.com/jeffclay)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jeffclay "Code") | [<img src="https://avatars.githubusercontent.com/u/52716446?v=4" width="110px;"/><br /><sub>Phil J R</sub>](https://github.com/PP-JN-RL)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PP-JN-RL "Code") | [<img src="https://avatars.githubusercontent.com/u/1496725?v=4" width="110px;"/><br /><sub>i_virus</sub>](https://www.corelight.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chandanchowdhury "Code") | [<img src="https://avatars.githubusercontent.com/u/1020541?v=4" width="110px;"/><br /><sub>Paul Grime</sub>](https://github.com/gitgrimbo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gitgrimbo "Code") | [<img src="https://avatars.githubusercontent.com/u/922815?v=4" width="110px;"/><br /><sub>Lee Porte</sub>](https://leeporte.co.uk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=LeePorte "Code") | [<img src="https://avatars.githubusercontent.com/u/23613427?v=4" width="110px;"/><br /><sub>BRYAN </sub>](https://github.com/bryanlopezinc)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bryanlopezinc "Code") [⚠️](https://github.com/snipe/snipe-it/commits?author=bryanlopezinc "Tests") |
| [<img src="https://avatars.githubusercontent.com/u/64061710?v=4" width="110px;"/><br /><sub>U-H-T</sub>](https://github.com/U-H-T)<br />[💻](https://github.com/snipe/snipe-it/commits?author=U-H-T "Code") | [<img src="https://avatars.githubusercontent.com/u/5395363?v=4" width="110px;"/><br /><sub>Matt Tyree</sub>](https://github.com/Tyree)<br />[📖](https://github.com/snipe/snipe-it/commits?author=Tyree "Documentation") | [<img src="https://avatars.githubusercontent.com/u/292081?v=4" width="110px;"/><br /><sub>Florent Bervas</sub>](http://spoontux.net)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FlorentDotMe "Code") | [<img src="https://avatars.githubusercontent.com/u/4498077?v=4" width="110px;"/><br /><sub>Daniel Albertsen</sub>](https://ditscheri.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dbakan "Code") | [<img src="https://avatars.githubusercontent.com/u/100710244?v=4" width="110px;"/><br /><sub>r-xyz</sub>](https://github.com/r-xyz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=r-xyz "Code") | [<img src="https://avatars.githubusercontent.com/u/47491036?v=4" width="110px;"/><br /><sub>Steven Mainor</sub>](https://github.com/DrekiDegga)<br />[💻](https://github.com/snipe/snipe-it/commits?author=DrekiDegga "Code") | [<img src="https://avatars.githubusercontent.com/u/65785975?v=4" width="110px;"/><br /><sub>arne-kroeger</sub>](https://github.com/arne-kroeger)<br />[💻](https://github.com/snipe/snipe-it/commits?author=arne-kroeger "Code") |
| [<img src="https://avatars.githubusercontent.com/u/167117705?v=4" width="110px;"/><br /><sub>Glukose1</sub>](https://github.com/Glukose1)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Glukose1 "Code") |
| [<img src="https://avatars.githubusercontent.com/u/167117705?v=4" width="110px;"/><br /><sub>Glukose1</sub>](https://github.com/Glukose1)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Glukose1 "Code") | [<img src="https://avatars.githubusercontent.com/u/1197791?v=4" width="110px;"/><br /><sub>Scarzy</sub>](https://github.com/Scarzy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Scarzy "Code") |
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!

View file

@ -79,12 +79,12 @@ USER root
VOLUME ["/var/lib/snipeit"]
# Entrypoints
COPY docker/entrypoint_alpine.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# Startup script
COPY docker/startup_alpine.sh /startup.sh
RUN chmod +x /startup.sh
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["/entrypoint.sh"]
CMD ["/startup.sh"]
EXPOSE 80

View file

@ -97,7 +97,7 @@ RUN set -eux; \
VOLUME [ "/var/lib/snipeit" ]
COPY --chown=www-data:www-data docker/docker-secrets.env /var/www/html/.env
COPY --chmod=655 docker/docker-entrypoint.sh /usr/local/bin/docker-snipeit-entrypoint
COPY --chmod=655 docker/startup_alpine_fpm.sh /startup.sh
COPY docker/column-statistics.cnf /etc/mysql/conf.d/column-statistics.cnf
ENTRYPOINT [ "/usr/local/bin/docker-snipeit-entrypoint" ]
CMD [ "/usr/local/bin/docker-php-entrypoint", "php-fpm" ]
ENTRYPOINT [ "/startup.sh" ]
CMD [ "/startup.sh", "php-fpm" ]

View file

@ -0,0 +1,200 @@
<?php
namespace App\Http\Controllers\Api;
use App\Helpers\StorageHelper;
use Illuminate\Support\Facades\Storage;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Models\AssetModel;
use App\Models\Actionlog;
use App\Http\Requests\UploadFileRequest;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
/**
* This class controls file related actions related
* to assets for the Snipe-IT Asset Management application.
*
* Based on the Assets/AssetFilesController by A. Gianotto <snipe@snipe.net>
*
* @version v1.0
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
class AssetModelFilesController extends Controller
{
/**
* Accepts a POST to upload a file to the server.
*
* @param \App\Http\Requests\UploadFileRequest $request
* @param int $assetModelId
* @since [v7.0.12]
* @author [r-xyz]
*/
public function store(UploadFileRequest $request, $assetModelId = null) : JsonResponse
{
// Start by checking if the asset being acted upon exists
if (! $assetModel = AssetModel::find($assetModelId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404);
}
// Make sure we are allowed to update this asset
$this->authorize('update', $assetModel);
if ($request->hasFile('file')) {
// If the file storage directory doesn't exist; create it
if (! Storage::exists('private_uploads/assetmodels')) {
Storage::makeDirectory('private_uploads/assetmodels', 775);
}
// Loop over the attached files and add them to the asset
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/assetmodels/','model-'.$assetModel->id, $file);
$assetModel->logUpload($file_name, e($request->get('notes')));
}
// All done - report success
return response()->json(Helper::formatStandardApiResponse('success', $assetModel, trans('admin/models/message.upload.success')));
}
// We only reach here if no files were included in the POST, so tell the user this
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.upload.nofiles')), 500);
}
/**
* List the files for an asset.
*
* @param int $assetModelId
* @since [v7.0.12]
* @author [r-xyz]
*/
public function list($assetModelId = null) : JsonResponse
{
// Start by checking if the asset being acted upon exists
if (! $assetModel = AssetModel::find($assetModelId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404);
}
// the asset is valid
if (isset($assetModel->id)) {
$this->authorize('view', $assetModel);
// Check that there are some uploads on this asset that can be listed
if ($assetModel->uploads->count() > 0) {
$files = array();
foreach ($assetModel->uploads as $upload) {
array_push($files, $upload);
}
// Give the list of files back to the user
return response()->json(Helper::formatStandardApiResponse('success', $files, trans('admin/models/message.upload.success')));
}
// There are no files.
return response()->json(Helper::formatStandardApiResponse('success', array(), trans('admin/models/message.upload.success')));
}
// Send back an error message
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.download.error')), 500);
}
/**
* Check for permissions and display the file.
*
* @param int $assetModelId
* @param int $fileId
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
* @since [v7.0.12]
* @author [r-xyz]
*/
public function show($assetModelId = null, $fileId = null) : JsonResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse
{
// Start by checking if the asset being acted upon exists
if (! $assetModel = AssetModel::find($assetModelId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404);
}
// the asset is valid
if (isset($assetModel->id)) {
$this->authorize('view', $assetModel);
// Check that the file being requested exists for the asset
if (! $log = Actionlog::whereNotNull('filename')->where('item_id', $assetModel->id)->find($fileId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.download.no_match', ['id' => $fileId])), 404);
}
// Form the full filename with path
$file = 'private_uploads/assetmodels/'.$log->filename;
Log::debug('Checking for '.$file);
if ($log->action_type == 'audit') {
$file = 'private_uploads/audits/'.$log->filename;
}
// Check the file actually exists on the filesystem
if (! Storage::exists($file)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.download.does_not_exist', ['id' => $fileId])), 404);
}
if (request('inline') == 'true') {
$headers = [
'Content-Disposition' => 'inline',
];
return Storage::download($file, $log->filename, $headers);
}
return StorageHelper::downloader($file);
}
// Send back an error message
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.download.error', ['id' => $fileId])), 500);
}
/**
* Delete the associated file
*
* @param int $assetModelId
* @param int $fileId
* @since [v7.0.12]
* @author [r-xyz]
*/
public function destroy($assetModelId = null, $fileId = null) : JsonResponse
{
// Start by checking if the asset being acted upon exists
if (! $assetModel = AssetModel::find($assetModelId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404);
}
$rel_path = 'private_uploads/assetmodels';
// the asset is valid
if (isset($assetModel->id)) {
$this->authorize('update', $assetModel);
// Check for the file
$log = Actionlog::find($fileId);
if ($log) {
// Check the file actually exists, and delete it
if (Storage::exists($rel_path.'/'.$log->filename)) {
Storage::delete($rel_path.'/'.$log->filename);
}
// Delete the record of the file
$log->delete();
// All deleting done - notify the user of success
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/models/message.deletefile.success')), 200);
}
// The file doesn't seem to really exist, so report an error
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.deletefile.error')), 500);
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.deletefile.error')), 500);
}
}

View file

@ -851,6 +851,24 @@ th.css-component > .th-inner::before
margin-top:50px
}
}
@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{
left:auto;
order:1;
}
.col-md-9.col-xs-12.col-sm-pull-3.info-stack{
right:auto;
order:2;
}
.info-stack-container > .col-md-9.col-xs-12.col-sm-pull-3.info-stack > .row-new-striped > .row > .col-sm-2{
width:auto;
float:none;
}
}
@media screen and (max-width: 1318px) and (min-width: 1200px){
.admin.box{
height:170px;

View file

@ -43,5 +43,11 @@ return array(
'success' => 'Model deleted!|:success_count models deleted!',
'success_partial' => ':success_count model(s) were deleted, however :fail_count were unable to be deleted because they still have assets associated with them.'
),
'download' => [
'error' => 'File(s) not downloaded. Please try again.',
'success' => 'File(s) successfully downloaded.',
'does_not_exist' => 'No file exists',
'no_match' => 'No matching record for that asset/file',
],
);

View file

@ -561,5 +561,6 @@ return [
'remaining_var' => ':count Remaining',
'label' => 'Label',
'import_asset_tag_exists' => 'An asset with the asset tag :asset_tag already exists and an update was not requested. No change was made.',
'countries_manually_entered_help' => 'Values with an asterisk (*) were manually entered and do not match existing ISO 3166 dropdown values',
];

View file

@ -135,6 +135,7 @@ return [
'EC'=>'Ecuador',
'EE'=>'Estonia',
'EG'=>'Egypt',
'GB-ENG'=>'England',
'ER'=>'Eritrea',
'ES'=>'Spain',
'ET'=>'Ethiopia',
@ -233,6 +234,7 @@ return [
'NG'=>'Nigeria',
'NI'=>'Nicaragua',
'NL'=>'Netherlands',
'GB-NIR' => 'Northern Ireland',
'NO'=>'Norway',
'NP'=>'Nepal',
'NR'=>'Nauru',
@ -260,7 +262,7 @@ return [
'RU'=>'Russian Federation',
'RW'=>'Rwanda',
'SA'=>'Saudi Arabia',
'UK'=>'Scotland',
'GB-SCT'=>'Scotland',
'SB'=>'Solomon Islands',
'SC'=>'Seychelles',
'SS'=>'South Sudan',
@ -312,6 +314,7 @@ return [
'VI'=>'Virgin Islands (U.S.)',
'VN'=>'Viet Nam',
'VU'=>'Vanuatu',
'GB-WLS' =>'Wales',
'WF'=>'Wallis And Futuna Islands',
'WS'=>'Samoa',
'YE'=>'Yemen',

View file

@ -32,18 +32,27 @@ Form::macro('countries', function ($name = 'country', $selected = null, $class =
$idclause = (!is_null($id)) ? $id : '';
$select = '<select name="'.$name.'" class="'.$class.'" style="width:100%" '.$idclause.' aria-label="'.$name.'" data-placeholder="'.trans('localizations.select_country').'">';
// Pull the autoglossonym array from the localizations translation file
$countries_array = trans('localizations.countries');
$select = '<select name="'.$name.'" class="'.$class.'" style="width:100%" '.$idclause.' aria-label="'.$name.'" data-placeholder="'.trans('localizations.select_country').'" data-allow-clear="true" data-tags="true">';
$select .= '<option value="" role="option">'.trans('localizations.select_country').'</option>';
// Pull the autoglossonym array from the localizations translation file
foreach (trans('localizations.countries') as $abbr => $country) {
foreach ($countries_array as $abbr => $country) {
// We have to handle it this way to handle deprecication warnings since you can't strtoupper on null
if ($abbr!='') {
$abbr = strtoupper($abbr);
}
$select .= '<option value="'.$abbr.'"'.(($selected == $abbr) ? ' selected="selected" role="option" aria-selected="true"' : ' aria-selected="false"').'>'.$country.'</option> ';
// Loop through the countries configured in the localization file
$select .= '<option value="'.$abbr.'" selected="selected" role="option" '.(($selected == $abbr) ? ' selected="selected" role="option" aria-selected="true"' : ' aria-selected="false"').'>'.$country.'</option> ';
}
// If the country value doesn't exist in the array, add it as a new option and select it so we don't drop that data
if (!in_array($selected, $countries_array)) {
$select .= '<option value="' . $selected . '" selected="selected" role="option" aria-selected="true">' . $selected .' *</option> ';
}
$select .= '</select>';

View file

@ -160,9 +160,9 @@
</div>
</div>
@endif
<div class="info-stack-container">
<!-- Start button column -->
<div class="col-md-3 col-xs-12 col-sm-push-9">
<div class="col-md-3 col-xs-12 col-sm-push-9 info-stack">
<div class="col-md-12 text-center">
@if (($asset->image) || (($asset->model) && ($asset->model->image!='')))
@ -334,7 +334,7 @@
<!-- End button column -->
<div class="col-md-9 col-xs-12 col-sm-pull-3">
<div class="col-md-9 col-xs-12 col-sm-pull-3 info-stack">
<div class="row-new-striped">
@ -1078,6 +1078,7 @@
</div> <!--/end striped container-->
</div> <!-- end col-md-9 -->
</div><!-- end info-stack-container -->
</div> <!--/.row-->
</div><!-- /.tab-pane -->
@ -1120,7 +1121,7 @@
</table>
@else
<div class="alert alert-info alert-block">
<div class="alert alert-info alert-block hidden-print">
<x-icon type="info-circle" />
{{ trans('general.no_results') }}
</div>
@ -1157,7 +1158,7 @@
<td>{{ Helper::formatCurrencyOutput($component->purchase_cost) }} each</td>
<td>{{ $component->serial }}</td>
<td>
<a href="{{ route('components.checkin.show', $component->pivot->id) }}" class="btn btn-sm bg-purple" data-tooltip="true">{{ trans('general.checkin') }}</a>
<a href="{{ route('components.checkin.show', $component->pivot->id) }}" class="btn btn-sm bg-purple hidden-print" data-tooltip="true">{{ trans('general.checkin') }}</a>
</td>
<?php $totalCost = $totalCost + ($component->purchase_cost *$component->pivot->assigned_qty) ?>
@ -1175,7 +1176,7 @@
</tfoot>
</table>
@else
<div class="alert alert-info alert-block">
<div class="alert alert-info alert-block hidden-print">
<x-icon type="info-circle" />
{{ trans('general.no_results') }}
</div>
@ -1239,7 +1240,7 @@
@else
<div class="alert alert-info alert-block">
<div class="alert alert-info alert-block hidden-print">
<x-icon type="info-circle" />
{{ trans('general.no_results') }}
</div>
@ -1399,11 +1400,11 @@
</td>
<td>
@if (($file->filename) && (Storage::exists('private_uploads/assets/'.$file->filename)))
<a href="{{ route('show/assetfile', [$asset->id, $file->id, 'download'=>'true']) }}" class="btn btn-sm btn-default">
<a href="{{ route('show/assetfile', [$asset->id, $file->id, 'download'=>'true']) }}" class="btn btn-sm btn-default hidden-print">
<x-icon type="download" />
</a>
<a href="{{ route('show/assetfile', [$asset->id, $file->id, 'inline'=>'true']) }}" class="btn btn-sm btn-default" target="_blank">
<a href="{{ route('show/assetfile', [$asset->id, $file->id, 'inline'=>'true']) }}" class="btn btn-sm btn-default hidden-print" target="_blank">
<x-icon type="external-link" />
</a>
@endif
@ -1415,7 +1416,7 @@
</td>
<td>
@can('update', \App\Models\Asset::class)
<a class="btn delete-asset btn-sm btn-danger btn-sm" href="{{ route('delete/assetfile', [$asset->id, $file->id]) }}" data-tooltip="true" data-title="Delete" data-content="{{ trans('general.delete_confirm', ['item' => $file->filename]) }}">
<a class="btn delete-asset btn-sm btn-danger btn-sm hidden-print" href="{{ route('delete/assetfile', [$asset->id, $file->id]) }}" data-tooltip="true" data-title="Delete" data-content="{{ trans('general.delete_confirm', ['item' => $file->filename]) }}">
<x-icon type="delete" />
</a>
@endcan
@ -1427,7 +1428,7 @@
@else
<div class="alert alert-info alert-block">
<div class="alert alert-info alert-block hidden-print">
<x-icon type="info-circle" />
{{ trans('general.no_results') }}
</div>
@ -1503,12 +1504,12 @@
</td>
<td>
@if (($file->filename) && (Storage::exists('private_uploads/assetmodels/'.$file->filename)))
<a href="{{ route('show/modelfile', [$asset->model->id, $file->id]) }}" class="btn btn-sm btn-default">
<x-icon type="download" />
<a href="{{ route('show/modelfile', [$asset->model->id, $file->id]) }}" class="btn btn-sm btn-default hidden-print">
<x-icon type="download" class="hidden-print" />
</a>
<a href="{{ route('show/modelfile', [$asset->model->id, $file->id, 'inline'=>'true']) }}" class="btn btn-sm btn-default" target="_blank">
<x-icon type="external-link" />
<a href="{{ route('show/modelfile', [$asset->model->id, $file->id, 'inline'=>'true']) }}" class="btn btn-sm btn-default hidden-print" target="_blank">
<x-icon type="external-link" class="hidden-print" />
</a>
@endif
@ -1520,8 +1521,8 @@
</td>
<td>
@can('update', \App\Models\AssetModel::class)
<a class="btn delete-asset btn-sm btn-danger btn-sm" href="{{ route('delete/modelfile', [$asset->model->id, $file->id]) }}" data-tooltip="true" data-title="Delete" data-content="{{ trans('general.delete_confirm', ['item' => $file->filename]) }}">
<x-icon type="delete" /></i>
<a class="btn delete-asset btn-sm btn-danger btn-sm hidden-print" href="{{ route('delete/modelfile', [$asset->model->id, $file->id]) }}" data-tooltip="true" data-title="Delete" data-content="{{ trans('general.delete_confirm', ['item' => $file->filename]) }}">
<x-icon type="delete" class="hidden-print"/></i>
</a>
@endcan
</td>

View file

@ -35,6 +35,7 @@
{{ Form::label('country', trans('general.country'), array('class' => 'col-md-3 control-label')) }}
<div class="col-md-7">
{!! Form::countries('country', old('country', $item->country), 'select2') !!}
<p class="help-block">{{ trans('general.countries_manually_entered_help') }}</p>
{!! $errors->first('country', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
</div>
</div>

View file

@ -451,6 +451,8 @@
<label class="col-md-3 control-label" for="country">{{ trans('general.country') }}</label>
<div class="col-md-6">
{!! Form::countries('country', old('country', $user->country), 'col-md-12 select2') !!}
<p class="help-block">{{ trans('general.countries_manually_entered_help') }}</p>
{!! $errors->first('country', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
</div>

View file

@ -159,9 +159,9 @@
</div>
</div>
@endif
<div class="info-stack-container">
<!-- Start button column -->
<div class="col-md-3 col-xs-12 col-sm-push-9">
<div class="col-md-3 col-xs-12 col-sm-push-9 info-stack">
@ -197,7 +197,9 @@
{{ trans('admin/users/general.print_assigned') }}
</a>
@else
<button style="width: 100%;" class="btn btn-sm btn-primary hidden-print" rel="noopener" disabled title="{{ trans('admin/users/message.user_has_no_assets_assigned') }}">{{ trans('admin/users/general.print_assigned') }}</button>
<button style="width: 100%;" class="btn btn-sm btn-primary btn-social hidden-print" rel="noopener" disabled title="{{ trans('admin/users/message.user_has_no_assets_assigned') }}">
<x-icon type="print" />
{{ trans('admin/users/general.print_assigned') }}</button>
@endif
</div>
@endcan
@ -306,7 +308,7 @@
<!-- End button column -->
<div class="col-md-9 col-xs-12 col-sm-pull-3">
<div class="col-md-9 col-xs-12 col-sm-pull-3 info-stack">
<div class="row-new-striped">
@ -765,6 +767,7 @@
@endif
</div> <!--/end striped container-->
</div> <!-- end col-md-9 -->
</div><!-- end info-stack-container-->
</div> <!--/.row-->
</div><!-- /.tab-pane -->

View file

@ -798,6 +798,33 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'throttle:api']], functi
]
)->name('api.models.restore');
Route::post('{model_id}/files',
[
Api\AssetModelFilesController::class,
'store'
]
)->name('api.models.files.store');
Route::get('{model_id}/files',
[
Api\AssetModelFilesController::class,
'list'
]
)->name('api.models.files.index');
Route::get('{model_id}/file/{file_id}',
[
Api\AssetModelFilesController::class,
'show'
]
)->name('api.models.files.show');
Route::delete('{model_id}/file/{file_id}',
[
Api\AssetModelFilesController::class,
'destroy'
]
)->name('api.models.files.destroy');
});
Route::resource('models',

View file

@ -0,0 +1,120 @@
<?php
namespace Tests\Feature\AssetModels\Api;
use App\Models\AssetModel;
use App\Models\User;
use Illuminate\Http\UploadedFile;
use Tests\TestCase;
class AssetModelFilesTest extends TestCase
{
public function testAssetModelApiAcceptsFileUpload()
{
// Upload a file to a model
// Create a model to work with
$model = AssetModel::factory()->count(1)->create();
// Create a superuser to run this as
$user = User::factory()->superuser()->create();
//Upload a file
$this->actingAsForApi($user)
->post(
route('api.models.files.store', ['model_id' => $model[0]["id"]]), [
'file' => [UploadedFile::fake()->create("test.jpg", 100)]
])
->assertOk();
}
public function testAssetModelApiListsFiles()
{
// List all files on a model
// Create an model to work with
$model = AssetModel::factory()->count(1)->create();
// Create a superuser to run this as
$user = User::factory()->superuser()->create();
// List the files
$this->actingAsForApi($user)
->getJson(
route('api.models.files.index', ['model_id' => $model[0]["id"]]))
->assertOk()
->assertJsonStructure([
'status',
'messages',
'payload',
]);
}
public function testAssetModelApiDownloadsFile()
{
// Download a file from a model
// Create a model to work with
$model = AssetModel::factory()->count(1)->create();
// Create a superuser to run this as
$user = User::factory()->superuser()->create();
//Upload a file
$this->actingAsForApi($user)
->post(
route('api.models.files.store', ['model_id' => $model[0]["id"]]), [
'file' => [UploadedFile::fake()->create("test.jpg", 100)]
])
->assertOk();
// List the files to get the file ID
$result = $this->actingAsForApi($user)
->getJson(
route('api.models.files.index', ['model_id' => $model[0]["id"]]))
->assertOk();
// Get the file
$this->actingAsForApi($user)
->get(
route('api.models.files.show', [
'model_id' => $model[0]["id"],
'file_id' => $result->decodeResponseJson()->json()["payload"][0]["id"],
]))
->assertOk();
}
public function testAssetModelApiDeletesFile()
{
// Delete a file from a model
// Create a model to work with
$model = AssetModel::factory()->count(1)->create();
// Create a superuser to run this as
$user = User::factory()->superuser()->create();
//Upload a file
$this->actingAsForApi($user)
->post(
route('api.models.files.store', ['model_id' => $model[0]["id"]]), [
'file' => [UploadedFile::fake()->create("test.jpg", 100)]
])
->assertOk();
// List the files to get the file ID
$result = $this->actingAsForApi($user)
->getJson(
route('api.models.files.index', ['model_id' => $model[0]["id"]]))
->assertOk();
// Delete the file
$this->actingAsForApi($user)
->delete(
route('api.models.files.destroy', [
'model_id' => $model[0]["id"],
'file_id' => $result->decodeResponseJson()->json()["payload"][0]["id"],
]))
->assertOk();
}
}