Merge branch 'label_generator'

This commit is contained in:
Jan Böhmer 2020-05-10 14:19:29 +02:00
commit c0f5952994
118 changed files with 10459 additions and 3205 deletions

View file

@ -833,3 +833,8 @@ div.dataTables_wrapper div.dataTables_info {
.darkmode--activated img {
mix-blend-mode: difference;
}
.scanner-video {
max-width: 500px;
max-height: 250px;
}

View file

@ -23,6 +23,7 @@ import "marked";
import * as marked from "marked";
import "qrcode";
import {parse} from "marked";
import * as ZXing from "@zxing/library";
/************************************
*
@ -568,6 +569,91 @@ $(document).on("ajaxUI:reload", function() {
})
});
//Reuse codereader between multiple requests
const codeReader = new ZXing.BrowserMultiFormatReader();
//Init barcode scanner
$(document).on("ajaxUI:start ajaxUI:reload", function() {
//Skip if we are not on scanner page...
if (!document.getElementById('scan_dialog_form')) {
codeReader.reset();
return;
}
let selectedDeviceId;
//Save it for later, so we can reset it
console.log('ZXing code reader initialized');
codeReader.listVideoInputDevices()
.then((videoInputDevices) => {
if (videoInputDevices.length >= 1) {
const sourceSelect = document.getElementById('sourceSelect');
videoInputDevices.forEach((element) => {
const sourceOption = document.createElement('option');
sourceOption.text = element.label;
sourceOption.value = element.deviceId;
sourceSelect.appendChild(sourceOption);
});
//Try to retrieve last selected webcam...
let last_cam_id = localStorage.getItem('scanner_last_cam_id');
if (!!last_cam_id) {
//selectedDeviceId = localStorage.getItem('scanner_last_cam_id');
$(sourceSelect).val(last_cam_id);
} else {
selectedDeviceId = videoInputDevices[0].deviceId;
}
sourceSelect.onchange = () => {
//@ts-ignore
selectedDeviceId = sourceSelect.value;
localStorage.setItem('scanner_last_cam_id', selectedDeviceId);
changeHandler();
};
document.getElementById('sourceSelectPanel').classList.remove('d-none');
document.getElementById('video').classList.remove('d-none');
document.getElementById('scanner-warning').classList.add('d-none');
}
let changeHandler = () => {
codeReader.reset();
codeReader.decodeFromVideoDevice(selectedDeviceId, 'video', (result, err) => {
if (result) {
//@ts-ignore
document.getElementById('scan_dialog_input').value = result.text;
//Submit form
//@ts-ignore
document.getElementById('scan_dialog_form').submit();
}
if (err && !(err instanceof ZXing.NotFoundException)) {
console.error(err);
//document.getElementById('result').textContent = err
}
});
console.log(`Started continous decode from camera with id ${selectedDeviceId}`)
};
//Register Change Src Button
//document.getElementById('changeSrcBtn').addEventListener('click', changeHandler);
//Try to start logging automatically.
changeHandler();
})
.catch((err) => {
console.error(err)
})
});
//Need for proper body padding, with every navbar height
$(window).resize(function () {

View file

@ -13,6 +13,8 @@
"beberlei/doctrineextensions": "^1.2",
"doctrine/annotations": "^1.6",
"doctrine/doctrine-bundle": "^2.0",
"dompdf/dompdf": "^0.8.5",
"erusev/parsedown": "^1.7",
"florianv/swap": "^4.0",
"friendsofsymfony/ckeditor-bundle": "^2.0",
"gregwar/captcha-bundle": "^2.1.0",
@ -50,8 +52,10 @@
"symfony/web-link": "4.4.*",
"symfony/webpack-encore-bundle": "^1.1",
"symfony/yaml": "4.4.*",
"tecnickcom/tc-lib-barcode": "^1.15",
"twig/cssinliner-extra": "^3.0",
"twig/extra-bundle": "^3.0",
"twig/html-extra": "^3.0",
"twig/inky-extra": "^3.0",
"twig/intl-extra": "^3.0",
"twig/markdown-extra": "^3.0",

1846
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -17,7 +17,19 @@ fos_ck_editor:
extraPlugins: "markdown"
height: 60
toolbar: description_toolbar
label_config:
height: 100
enterMode: 2
toolbar: label_toolbar
extraPlugins: ["partdb_label", "showprotected"]
allowedContent: true
font_names: >
DejaVu Sans Mono/DejaVu Sans Mono;
DejaVu Sans/DejaVu Sans;
DejaVu Serif/DejaVu Serif;
Helvetica/Helvetica, Arial, sans-serif;
Times New Roman/Times New Roman, Times, serif;
Courier New/Courier New, Courier, monospace;
plugins:
bbcode:
@ -29,10 +41,25 @@ fos_ck_editor:
specialchar:
path: "ckeditor/plugins/specialchar"
filename: "plugin.js"
partdb_label:
path: "ckeditor/plugins/partdb_label/"
filename: "plugin.js"
showprotected:
path: "ckeditor/plugins/showprotected/"
filename: "plugin.js"
toolbars:
configs:
label_toolbar:
- [ 'Bold', 'Italic', 'Strike', 'Subscript', 'Superscript', '-', 'RemoveFormat' ]
- ['JustifyLeft', 'JustifyCenter', 'JustifyRight']
- ["SpecialChar"]
- ["Source"]
- "/"
- ['Format', 'FontSize', 'Font']
- ['Table', 'HorizontalRule']
- ['Placeholders']
description_toolbar:
- [ 'Bold', 'Italic', 'Strike', 'Subscript', 'Superscript', '-', 'RemoveFormat' ]
- ["SpecialChar"]

View file

@ -15,3 +15,4 @@ twig:
allow_email_pw_reset: '%allow_email_pw_reset%'
locale_menu: '%locale_menu%'
attachment_manager: '@App\Services\Attachments\AttachmentManager'
label_profile_dropdown_helper: '@App\Services\LabelSystem\LabelProfileDropdownHelper'

View file

@ -291,6 +291,9 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co
timetravel:
label: "perm.tools.timeTravel"
bit: 14
label_scanner:
label: "perm.tools.label_scanner"
bit: 16
groups:
label: "perm.groups"
@ -466,11 +469,33 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co
edit_options:
label: "perm.self.edit_options"
bit: 2
delete_profiles:
label: "perm.self.delete_profiles"
bit: 4
alsoSet: ['create_labels']
read_profiles:
label: "perm.self.read_profiles"
bit: 10
edit_profiles:
label: "perm.self.edit_profiles"
bit: 6
alsoSet: ['read_profiles']
create_profiles:
label: "perm.self.create_profiles"
bit: 8
alsoSet: ['read_profiles', 'edit_profiles']
delete_profiles:
label: "perm.self.delete_profiles"
bit: 4
alsoSet: ['read_profiles', 'edit_profiles', 'create_profiles']
use_twig:
label: "perm.labels.use_twig"
bit: 12
alsoSet: ['create_labels', 'edit_options']
show_history:
label: "perm.show_history"
bit: 14
alsoSet: ['read_profiles']
revert_element:
label: "perm.revert_elements"
bit: 16
alsoSet: ['read_profiles', 'edit_profiles', 'create_profiles', 'delete_profiles']

View file

@ -3,6 +3,11 @@
# controller: App\Controller\DefaultController::index
# Redirect every url without an locale to the locale of the user/the global base locale
scan_qr:
path: /scan/{type}/{id}
controller: App\Controller\ScanController:scanQRCode
redirector:
path: /{url}
requirements:

View file

@ -35,6 +35,11 @@ services:
bool $gpdr_compliance : '%gpdr_compliance%'
bool $kernel_debug: '%kernel.debug%'
string $kernel_cache_dir: '%kernel.cache_dir%'
string $partdb_title: '%partdb_title%'
_instanceof:
App\Services\LabelSystem\PlaceholderProviders\PlaceholderProviderInterface:
tags: ['app.label_placeholder_provider']
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
@ -163,6 +168,15 @@ services:
$code_length: 8
$code_count: 15
App\Services\LabelSystem\LabelTextReplacer:
arguments:
$providers: !tagged_iterator 'app.label_placeholder_provider'
App\Services\TranslationExtractor\PermissionExtractor:
tags:
- { name: 'translation.extractor', alias: 'permissionExtractor'}
# PartLotProvider must be invoked before all other providers, so it can override %%NAME%% placeholder
App\Services\LabelSystem\PlaceholderProviders\PartLotProvider:
tags:
- { name: 'app.label_placeholder_provider', priority: 10}

View file

@ -25,6 +25,7 @@
"@types/jquery.form": "^3.26.30",
"@types/marked": "^0.7.2",
"@types/typeahead": "^0.11.32",
"@zxing/library": "^0.16.3",
"bootbox": "^5.4.0",
"bootstrap-fileinput": "^5.0.1",
"bootstrap-select": "^1.13.8",

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1,69 @@
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
CKEDITOR.plugins.setLang( 'partdb_label', 'de', {
title: 'Platzhalter einfügen',
label: 'Platzhalter',
'section.global': 'Global',
'section.part': 'Bauteil',
'section.part_lot': 'Bauteilbestand',
'section.storelocation': 'Lagerort',
'part.id': 'Datenbank ID',
'part.name': 'Bauteilename',
'part.category': 'Kategorie',
'part.category_full': 'Kategorie (Ganzer Pfad)',
'part.manufacturer': 'Hersteller',
'part.manufacturer_full': 'Hersteller (Ganzer Pfad)',
'part.footprint': 'Footprint',
'part.footprint_full': 'Footprint (Ganzer Pfad)',
'part.mass': 'Gewicht',
'part.tags': 'Tags',
'part.mpn': 'Herstellernummer (MPN)',
'part.status': 'Herstellungsstatus',
'part.description': 'Beschreibung',
'part.description_t': 'Beschreibung (Text)',
'part.comment': 'Kommentar',
'part.comment_t': 'Kommentar (Text)',
'part.last_modified': 'Änderungsdatum',
'part.creation_date': 'Erstellungsdatum',
'global.username': 'Benutzername',
'global.username_full': 'Benutzername (inklusive Name)',
'global.datetime': 'Datum & Uhrzeit',
'global.date': 'Datum',
'global.time': 'Uhrzeit',
'global.install_name': 'Installationsname',
'global.type': 'Zieltyp',
'lot.id': 'Lot ID',
'lot.name': 'Lot Name',
'lot.comment': 'Lot Kommentar',
'lot.expiration_date': 'Ablaufdatum',
'lot.amount': 'Bestandsmenge',
'lot.location': 'Lagerort',
'lot.location_full': 'Lagerort (Ganzer Pfad)',
'storelocation.id': 'Lagerort ID',
'storelocation.name': 'Name',
'storelocation.full_path': 'Ganzer Pfad',
'storelocation.parent_name': 'Name des Übergeordneten Elements',
'storelocation.parent_full_path': 'Ganzer Pfad des Übergeordneten Elements',
'storelocation.comment': 'Kommentar',
'storelocation.comment_t': 'Kommentar (Text)',
'storelocation.last_modified': 'Änderungsdatum',
'storelocation.creation_date': 'Erstellungsdatum',
} );

View file

@ -0,0 +1,69 @@
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
CKEDITOR.plugins.setLang( 'partdb_label', 'en', {
title: 'Insert Placeholders',
label: 'Placeholders',
'section.global': 'Globals',
'section.part': 'Part',
'section.part_lot': 'Part lot',
'section.storelocation': 'Storage location',
'part.id': 'Database ID',
'part.name': 'Part name',
'part.category': 'Category',
'part.category_full': 'Category (Full path)',
'part.manufacturer': 'Manufacturer',
'part.manufacturer_full': 'Manufacturer (Full path)',
'part.footprint': 'Footprint',
'part.footprint_full': 'Footprint (Full path)',
'part.mass': 'Mass',
'part.tags': 'Tags',
'part.mpn': 'Manufacturer Product Number (MPN)',
'part.status': 'Manufacturing status',
'part.description': 'Description',
'part.description_t': 'Description (Text)',
'part.comment': 'Comment',
'part.comment_t': 'Comment (Text)',
'part.last_modified': 'Last modified datetime',
'part.creation_date': 'Creation datetime',
'global.username': 'Username',
'global.username_full': 'Username (including name)',
'global.datetime': 'Current datetime',
'global.date': 'Current date',
'global.time': 'Current time',
'global.install_name': 'Instance name',
'global.type': 'Target type',
'lot.id': 'Lot ID',
'lot.name': 'Lot name',
'lot.comment': 'Lot comment',
'lot.expiration_date': 'Expiration date',
'lot.amount': 'Lot amount',
'lot.location': 'Storage location',
'lot.location_full': 'Storage location (Full path)',
'storelocation.id': 'Location ID',
'storelocation.name': 'Name',
'storelocation.full_path': 'Full path',
'storelocation.parent_name': 'Parent name',
'storelocation.parent_full_path': 'Parent full path',
'storelocation.comment': 'Comment',
'storelocation.comment_t': 'Comment (Text)',
'storelocation.last_modified': 'Last modified datetime',
'storelocation.creation_date': 'Createion datetime',
} );

View file

@ -0,0 +1,209 @@
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* Placeholder logic inspired by CKEDITOR placeholder plugin (https://github.com/ckeditor/ckeditor4/blob/master/plugins/placeholder/plugin.js)
*/
const PLACEHOLDERS = {
part: {
label: 'section.part',
entries: [
['[[ID]]', 'part.id'],
['[[NAME]]', 'part.name'],
['[[CATEGORY]]', 'part.category'],
['[[CATEGORY_FULL]]', 'part.category_full'],
['[[MANUFACTURER]]', 'part.manufacturer'],
['[[MANUFACTURER_FULL]]', 'part.manufacturer_full'],
['[[FOOTPRINT]]', 'part.footprint'],
['[[FOOTPRINT_FULL]]', 'part.footprint'],
['[[MASS]]', 'part.mass'],
['[[MPN]]', 'part.mpn'],
['[[TAGS]]', 'part.tags'],
['[[M_STATUS]]', 'part.status'],
['[[DESCRIPTION]]', 'part.description'],
['[[DESCRIPTION_T]]', 'part.description_t'],
['[[COMMENT]]', 'part.comment'],
['[[COMMENT_T]]', 'part.comment_t'],
['[[LAST_MODIFIED]]', 'part.last_modified'],
['[[CREATION_DATE]]', 'part.creation_date'],
]
},
part_lot: {
label: 'section.part_lot',
entries: [
['[[LOT_ID]]', 'lot.id'],
['[[LOT_NAME]]', 'lot.name'],
['[[LOT_COMMENT]]', 'lot.comment'],
['[[EXPIRATION_DATE]]', 'lot.expiration_date'],
['[[AMOUNT]]', 'lot.amount'],
['[[LOCATION]]', 'lot.location'],
['[[LOCATION_FULL]]', 'lot.location_full'],
]
},
storelocation: {
label: 'section.storelocation',
entries: [
['[[ID]]', 'storelocation.id'],
['[[NAME]]', 'storelocation.name'],
['[[FULL_PATH]]', 'storelocation.full_path'],
['[[PARENT]]', 'storelocation.parent_name'],
['[[PARENT_FULL_PATH]]', 'storelocation.parent_full_path'],
['[[COMMENT]]', 'storelocation.comment'],
['[[COMMENT_T]]', 'storelocation.comment_t'],
['[[LAST_MODIFIED]]', 'storelocation.last_modified'],
['[[CREATION_DATE]]', 'storelocation.creation_date'],
]
},
global: {
label: 'section.global',
entries: [
['[[USERNAME]]', 'global.username'],
['[[USERNAME_FULL]]', 'global.username_full'],
['[[DATETIME]]', 'global.datetime'],
['[[DATE]]', 'global.date'],
['[[TIME]]', 'global.time'],
['[[INSTALL_NAME]]', 'global.install_name'],
['[[TYPE]]', 'global.type']
],
},
};
function findLabelForPlaceholder(search)
{
for (var group in PLACEHOLDERS) {
var entries = PLACEHOLDERS[group].entries;
for (var placeholder in entries) {
if (entries[placeholder][0] == search) {
return entries[placeholder][1];
}
}
}
}
//Dont escape text inside of twig blocks
CKEDITOR.config.protectedSource.push(/\{\{[\s\S]*?\}\}/g);
CKEDITOR.config.protectedSource.push(/\{\%[\s\S]*?%\}/g);
CKEDITOR.plugins.add('partdb_label', {
hidpi: true,
icons: 'placeholder',
lang: ['en', 'de'],
init: function (editor) {
var config = editor.config,
lang = editor.lang.partdb_label;
var pluginDirectory = this.path;
editor.addContentsCss( pluginDirectory + 'styles/style.css' );
// Put ur init code here.
editor.widgets.add( 'placeholder', {
// Widget code.
pathName: lang.label,
// We need to have wrapping element, otherwise there are issues in
// add dialog.
template: '<span class="cke_placeholder">[[]]</span>',
downcast: function() {
return new CKEDITOR.htmlParser.text( '[[' + this.data.name + ']]' );
},
init: function() {
// Note that placeholder markup characters are stripped for the name.
this.setData( 'name', this.element.getText().slice( 2, -2 ) );
},
data: function() {
this.element.setText( '[[' + this.data.name + ']]' );
var title = findLabelForPlaceholder( '[[' + this.data.name + ']]');
if (lang[title]) {
title = lang[title];
}
this.element.setAttribute('title', title);
},
getLabel: function() {
return this.editor.lang.widget.label.replace( /%1/, this.data.name + ' ' + this.pathName );
}
} );
editor.ui.addRichCombo('Placeholders', {
label: lang.label,
title: lang.title,
allowedContent: 'abbr[title]',
panel: {
css: [ CKEDITOR.skin.getPath( 'editor' ) ].concat( config.contentsCss ),
multiSelect: false,
attributes: { 'aria-label': lang.title }
},
init: function () {
for (var group in PLACEHOLDERS) {
var localized_group = PLACEHOLDERS[group].label;
if (lang[localized_group]) {
localized_group = lang[localized_group];
}
this.startGroup(localized_group);
var entries = PLACEHOLDERS[group].entries;
for (var placeholder in entries) {
var localized_placeholder = entries[placeholder][1];
if (lang[localized_placeholder]) {
localized_placeholder = lang[localized_placeholder];
}
this.add(entries[placeholder][0], localized_placeholder, entries[placeholder][0])
}
}
},
onClick: function(value) {
editor.focus();
editor.fire('saveSnapshot');
editor.insertText(value);
}
});
},
afterInit: function( editor ) {
var placeholderReplaceRegex = /\[\[([^\[\]])+\]\]/g;
editor.dataProcessor.dataFilter.addRules({
text: function (text, node) {
var dtd = node.parent && CKEDITOR.dtd[node.parent.name];
// Skip the case when placeholder is in elements like <title> or <textarea>
// but upcast placeholder in custom elements (no DTD).
if (dtd && !dtd.span)
return;
return text.replace(placeholderReplaceRegex, function (match) {
// Creating widget code.
var widgetWrapper = null,
innerElement = new CKEDITOR.htmlParser.element('span', {
'class': 'cke_placeholder'
});
// Adds placeholder identifier as innertext.
innerElement.add(new CKEDITOR.htmlParser.text(match));
widgetWrapper = editor.widgets.wrapElement(innerElement, 'placeholder');
// Return outerhtml of widget wrapper so it will be placed
// as replacement.
return widgetWrapper.getOuterHtml();
});
}
});
}
});

View file

@ -0,0 +1,117 @@
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@font-face {
font-family: "DejaVu Sans Mono";
font-style: normal;
font-weight: normal;
src: local(DejaVu Sans Mono), local(DejaVuSansMono),
url(DejaVuSansMono.woff) format("woff");
}
@font-face {
font-family: "DejaVu Sans Mono";
font-style: normal;
font-weight: bold;
src: local(DejaVu Sans Mono Bold), local(DejaVuSansMono-Bold),
url(DejaVuSansMono-Bold.woff) format("woff");
}
@font-face {
font-family: "DejaVu Sans Mono";
font-style: oblique;
font-weight: bold;
src: local(DejaVu Sans Mono Bold Oblique), local(DejaVuSansMono-BoldOblique),
url(DejaVuSansMono-BoldOblique.woff) format("woff");
}
@font-face {
font-family: "DejaVu Sans Mono";
font-style: oblique;
font-weight: normal;
src: local(DejaVu Sans Mono Oblique), local(DejaVuSansMono-Oblique),
url(DejaVuSansMono-Oblique.woff) format("woff");
}
@font-face {
font-family: "DejaVu Sans";
font-style: normal;
font-weight: normal;
src: local(DejaVu Sans), local(DejaVuSans),
url(DejaVuSans.woff) format("woff");
}
@font-face {
font-family: "DejaVu Sans";
font-style: normal;
font-weight: bold;
src: local(DejaVu Sans Bold), local(DejaVuSans-Bold),
url(DejaVuSans-Bold.woff) format("woff");
}
@font-face {
font-family: "DejaVu Sans";
font-style: oblique;
font-weight: bold;
src: local(DejaVu Sans Bold Oblique), local(DejaVuSans-BoldOblique),
url(DejaVuSans-BoldOblique.woff) format("woff");
}
@font-face {
font-family: "DejaVu Sans";
font-style: oblique;
font-weight: normal;
src: local(DejaVu Sans Oblique), local(DejaVuSans-Oblique),
url(DejaVuSans-Oblique.woff) format("woff");
}
@font-face {
font-family: "DejaVu Serif";
font-style: normal;
font-weight: normal;
src: local(DejaVu Serif), local(DejaVuSerif),
url(DejaVuSerif.woff) format("woff");
}
@font-face {
font-family: "DejaVu Serif";
font-style: normal;
font-weight: bold;
src: local(DejaVu Serif Bold), local(DejaVuSerif-Bold),
url(DejaVuSerif-Bold.woff) format("woff");
}
@font-face {
font-family: "DejaVu Serif";
font-style: italic;
font-weight: bold;
src: local(DejaVu Serif Bold Italic), local(DejaVuSerif-BoldItalic),
url(DejaVuSerif-BoldItalic.woff) format("woff");
}
@font-face {
font-family: "DejaVu Serif";
font-style: italic;
font-weight: normal;
src: local(DejaVu Serif Italic), local(DejaVuSerif-Italic),
url(DejaVuSerif-Italic.woff) format("woff");
}
.cke_placeholder {
background-color:#ff0
}
.cke_editable {
font-family: "DejaVu Sans Mono";
font-size: 9pt;
line-height: 1.5;
}

View file

@ -0,0 +1,52 @@
CKEDITOR.dialog.add( 'showProtectedDialog', function( editor ) {
return {
title: 'Edit Protected Source',
minWidth: 300,
minHeight: 60,
onOk: function() {
var newSourceValue = this.getContentElement( 'info', 'txtProtectedSource' ).getValue();
var encodedSourceValue = CKEDITOR.plugins.showprotected.encodeProtectedSource( newSourceValue );
this._.selectedElement.setAttribute('data-cke-realelement', encodedSourceValue);
this._.selectedElement.setAttribute('title', newSourceValue);
this._.selectedElement.setAttribute('alt', newSourceValue);
},
onHide: function() {
delete this._.selectedElement;
},
onShow: function() {
this._.selectedElement = editor.getSelection().getSelectedElement();
var decodedSourceValue = CKEDITOR.plugins.showprotected.decodeProtectedSource( this._.selectedElement.getAttribute('data-cke-realelement') );
this.setValueOf( 'info', 'txtProtectedSource', decodedSourceValue );
},
contents: [
{
id: 'info',
label: 'Edit Protected Source',
accessKey: 'I',
elements: [
{
type: 'text',
id: 'txtProtectedSource',
label: 'Value',
required: true,
validate: function() {
if ( !this.getValue() ) {
alert( 'The value cannot be empty' );
return false;
}
return true;
}
}
]
}
]
};
} );

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 B

View file

@ -0,0 +1,105 @@
/*
* "showprotected" CKEditor plugin
*
* Created by Matthew Lieder (https://github.com/IGx89)
*
* Licensed under the MIT, GPL, LGPL and MPL licenses
*
* Icon courtesy of famfamfam: http://www.famfamfam.com/lab/icons/mini/
*/
// TODO: configuration settings
// TODO: show the actual text inline, not just an icon?
// TODO: improve copy/paste behavior (tooltip is wrong after paste)
CKEDITOR.plugins.add( 'showprotected', {
requires: 'dialog,fakeobjects',
onLoad: function() {
// Add the CSS styles for protected source placeholders.
var iconPath = CKEDITOR.getUrl( this.path + 'images' + '/code.gif' ),
baseStyle = 'background:url(' + iconPath + ') no-repeat %1 center;border:1px dotted #00f;background-size:16px;';
var template = '.%2 img.cke_protected' +
'{' +
baseStyle +
'width:16px;' +
'min-height:15px;' +
// The default line-height on IE.
'height:1.15em;' +
// Opera works better with "middle" (even if not perfect)
'vertical-align:' + ( CKEDITOR.env.opera ? 'middle' : 'text-bottom' ) + ';' +
'}';
// Styles with contents direction awareness.
function cssWithDir( dir ) {
return template.replace( /%1/g, dir == 'rtl' ? 'right' : 'left' ).replace( /%2/g, 'cke_contents_' + dir );
}
CKEDITOR.addCss( cssWithDir( 'ltr' ) + cssWithDir( 'rtl' ) );
},
init: function( editor ) {
CKEDITOR.dialog.add( 'showProtectedDialog', this.path + 'dialogs/protected.js' );
editor.on( 'doubleclick', function( evt ) {
var element = evt.data.element;
if ( element.is( 'img' ) && element.hasClass( 'cke_protected' ) ) {
evt.data.dialog = 'showProtectedDialog';
}
} );
},
afterInit: function( editor ) {
// Register a filter to displaying placeholders after mode change.
var dataProcessor = editor.dataProcessor,
dataFilter = dataProcessor && dataProcessor.dataFilter;
if ( dataFilter ) {
dataFilter.addRules( {
comment: function( commentText, commentElement ) {
if(commentText.indexOf(CKEDITOR.plugins.showprotected.protectedSourceMarker) == 0) {
commentElement.attributes = [];
var fakeElement = editor.createFakeParserElement( commentElement, 'cke_protected', 'protected' );
var cleanedCommentText = CKEDITOR.plugins.showprotected.decodeProtectedSource( commentText );
fakeElement.attributes.title = fakeElement.attributes.alt = cleanedCommentText;
return fakeElement;
}
return null;
}
} );
}
}
} );
/**
* Set of showprotected plugin's helpers.
*
* @class
* @singleton
*/
CKEDITOR.plugins.showprotected = {
protectedSourceMarker: '{cke_protected}',
decodeProtectedSource: function( protectedSource ) {
if(protectedSource.indexOf('%3C!--') == 0) {
return decodeURIComponent(protectedSource).replace( /<!--\{cke_protected\}([\s\S]+?)-->/g, function( match, data ) {
return decodeURIComponent( data );
} );
} else {
return decodeURIComponent(protectedSource.substr(CKEDITOR.plugins.showprotected.protectedSourceMarker.length));
}
},
encodeProtectedSource: function( protectedSource ) {
return '<!--' + CKEDITOR.plugins.showprotected.protectedSourceMarker +
encodeURIComponent( protectedSource ).replace( /--/g, '%2D%2D' ) +
'-->';
}
};

View file

@ -45,6 +45,8 @@ namespace App\Controller\AdminPages;
use App\DataTables\LogDataTable;
use App\Entity\Base\AbstractNamedDBElement;
use App\Entity\Base\AbstractStructuralDBElement;
use App\Entity\LabelSystem\LabelProfile;
use App\Entity\Parts\PartLot;
use App\Entity\UserSystem\User;
use App\Events\SecurityEvent;
use App\Events\SecurityEvents;
@ -54,6 +56,8 @@ use App\Form\AdminPages\MassCreationForm;
use App\Services\Attachments\AttachmentSubmitHandler;
use App\Services\EntityExporter;
use App\Services\EntityImporter;
use App\Services\LabelSystem\Barcodes\BarcodeExampleElementsGenerator;
use App\Services\LabelSystem\LabelGenerator;
use App\Services\LogSystem\EventCommentHelper;
use App\Services\LogSystem\HistoryHelper;
use App\Services\LogSystem\TimeTravel;
@ -92,11 +96,14 @@ abstract class BaseAdminController extends AbstractController
protected $dataTableFactory;
/** @var EventDispatcher */
protected $eventDispatcher;
protected $labelGenerator;
protected $barcodeExampleGenerator;
public function __construct(TranslatorInterface $translator, UserPasswordEncoderInterface $passwordEncoder,
AttachmentSubmitHandler $attachmentSubmitHandler,
EventCommentHelper $commentHelper, HistoryHelper $historyHelper, TimeTravel $timeTravel,
DataTableFactory $dataTableFactory, EventDispatcherInterface $eventDispatcher)
DataTableFactory $dataTableFactory, EventDispatcherInterface $eventDispatcher, BarcodeExampleElementsGenerator $barcodeExampleGenerator,
LabelGenerator $labelGenerator)
{
if ('' === $this->entity_class || '' === $this->form_class || '' === $this->twig_template || '' === $this->route_base) {
throw new InvalidArgumentException('You have to override the $entity_class, $form_class, $route_base and $twig_template value in your subclasss!');
@ -118,6 +125,8 @@ abstract class BaseAdminController extends AbstractController
$this->timeTravel = $timeTravel;
$this->dataTableFactory = $dataTableFactory;
$this->eventDispatcher = $eventDispatcher;
$this->barcodeExampleGenerator = $barcodeExampleGenerator;
$this->labelGenerator = $labelGenerator;
}
protected function _edit(AbstractNamedDBElement $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response
@ -156,11 +165,22 @@ abstract class BaseAdminController extends AbstractController
$table = null;
}
$form = $this->createForm($this->form_class, $entity, [
$form_options = [
'attachment_class' => $this->attachment_class,
'parameter_class' => $this->parameter_class,
'disabled' => null !== $timeTravel_timestamp ? true : null,
]);
];
//Disable editing of options, if user is not allowed to use twig...
if (
$entity instanceof LabelProfile
&& $entity->getOptions()->getLinesMode() === 'twig'
&& !$this->isGranted('@labels.use_twig')
) {
$form_options['disable_options'] = true;
}
$form = $this->createForm($this->form_class, $entity, $form_options);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
@ -210,11 +230,18 @@ abstract class BaseAdminController extends AbstractController
$this->addFlash('error', 'entity.edit_flash.invalid');
}
//Show preview for LabelProfile if needed.
if ($entity instanceof LabelProfile) {
$example = $this->barcodeExampleGenerator->getElement($entity->getOptions()->getSupportedElement());
$pdf_data = $this->labelGenerator->generateLabel($entity->getOptions(), $example);
}
return $this->render($this->twig_template, [
'entity' => $entity,
'form' => $form->createView(),
'route_base' => $this->route_base,
'datatable' => $table,
'pdf_data' => $pdf_data ?? null,
'timeTravel' => $timeTravel_timestamp,
]);
}

View file

@ -0,0 +1,126 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
namespace App\Controller\AdminPages;
use App\Entity\Attachments\AttachmentType;
use App\Entity\Attachments\AttachmentTypeAttachment;
use App\Entity\Attachments\LabelAttachment;
use App\Entity\LabelSystem\LabelProfile;
use App\Entity\Parameters\AttachmentTypeParameter;
use App\Form\AdminPages\AttachmentTypeAdminForm;
use App\Form\AdminPages\BaseEntityAdminForm;
use App\Form\AdminPages\LabelProfileAdminForm;
use App\Services\EntityExporter;
use App\Services\EntityImporter;
use App\Services\StructuralElementRecursionHelper;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/label_profile")
*/
class LabelProfileController extends BaseAdminController
{
protected $entity_class = LabelProfile::class;
protected $twig_template = 'AdminPages/LabelProfileAdmin.html.twig';
protected $form_class = LabelProfileAdminForm::class;
protected $route_base = 'label_profile';
protected $attachment_class = LabelAttachment::class;
protected $parameter_class = 'not_used';
/**
* @Route("/{id}", name="label_profile_delete", methods={"DELETE"})
*
* @return RedirectResponse
*/
public function delete(Request $request, LabelProfile $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse
{
return $this->_delete($request, $entity, $recursionHelper);
}
/**
* @Route("/{id}/edit/{timestamp}", requirements={"id"="\d+"}, name="label_profile_edit")
* @Route("/{id}", requirements={"id"="\d+"})
*
* @return Response
*/
public function edit(LabelProfile $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response
{
return $this->_edit($entity, $request, $em, $timestamp);
}
/**
* @Route("/new", name="label_profile_new")
* @Route("/{id}/clone", name="label_profile_clone")
* @Route("/")
*
* @return Response
*/
public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?AttachmentType $entity = null): Response
{
return $this->_new($request, $em, $importer, $entity);
}
/**
* @Route("/export", name="label_profile_export_all")
*
* @return Response
*/
public function exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response
{
return $this->_exportAll($em, $exporter, $request);
}
/**
* @Route("/{id}/export", name="label_profile_export")
*
* @return Response
*/
public function exportEntity(LabelProfile $entity, EntityExporter $exporter, Request $request): Response
{
return $this->_exportEntity($entity, $exporter, $request);
}
}

View file

@ -0,0 +1,167 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Controller;
use App\Entity\Base\AbstractDBElement;
use App\Entity\Contracts\NamedElementInterface;
use App\Entity\LabelSystem\LabelOptions;
use App\Entity\LabelSystem\LabelProfile;
use App\Entity\Parts\Part;
use App\Exceptions\TwigModeException;
use App\Form\LabelOptionsType;
use App\Form\LabelSystem\LabelDialogType;
use App\Helpers\LabelResponse;
use App\Repository\DBElementRepository;
use App\Services\ElementTypeNameGenerator;
use App\Services\LabelSystem\LabelGenerator;
use App\Services\Misc\RangeParser;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\SubmitButton;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @Route("/label")
* @package App\Controller
*/
class LabelController extends AbstractController
{
protected $labelGenerator;
protected $em;
protected $elementTypeNameGenerator;
protected $rangeParser;
protected $translator;
public function __construct(LabelGenerator $labelGenerator, EntityManagerInterface $em, ElementTypeNameGenerator $elementTypeNameGenerator,
RangeParser $rangeParser, TranslatorInterface $translator)
{
$this->labelGenerator = $labelGenerator;
$this->em = $em;
$this->elementTypeNameGenerator = $elementTypeNameGenerator;
$this->rangeParser = $rangeParser;
$this->translator = $translator;
}
/**
* @Route("/dialog", name="label_dialog")
* @Route("/{profile}/dialog", name="label_dialog_profile")
*/
public function generator(Request $request, ?LabelProfile $profile = null)
{
$this->denyAccessUnlessGranted('@labels.create_labels');
//If we inherit a LabelProfile, the user need to have access to it...
if ($profile !== null) {
$this->denyAccessUnlessGranted('read', $profile);
}
if ($profile) {
$label_options = $profile->getOptions();
} else {
$label_options = new LabelOptions();
}
//We have to disable the options, if twig mode is selected and user is not allowed to use it.
$disable_options = $label_options->getLinesMode() === 'twig' && !$this->isGranted("@labels.use_twig");
$form = $this->createForm(LabelDialogType::class, null, [
'disable_options' => $disable_options,
]);
//Try to parse given target_type and target_id
$target_type = $request->query->get('target_type', null);
$target_id = $request->query->get('target_id', null);
$generate = $request->query->getBoolean('generate', false);
if ($profile === null && is_string($target_type)) {
$label_options->setSupportedElement($target_type);
}
if (is_string($target_id)) {
$form['target_id']->setData($target_id);
}
$form['options']->setData($label_options);
$form->handleRequest($request);
/** @var LabelOptions $form_options */
$form_options = $form['options']->getData();
$pdf_data = null;
$filename = 'invalid.pdf';
//Generate PDF either when the form is submitted and valid, or the form was not submit yet, and generate is set
if (($form->isSubmitted() && $form->isValid()) || ($generate && !$form->isSubmitted() && $profile !== null)) {
$target_id = (string) $form->get('target_id')->getData();
$targets = $this->findObjects($form_options->getSupportedElement(), $target_id);
if (!empty($targets)) {
try {
$pdf_data = $this->labelGenerator->generateLabel($form_options, $targets);
$filename = $this->getLabelName($targets[0], $profile);
} catch (TwigModeException $exception) {
$form->get('options')->get('lines')->addError(new FormError($exception->getMessage()));
}
} else {
//$this->addFlash('warning', 'label_generator.no_entities_found');
$form->get('target_id')->addError(
new FormError($this->translator->trans('label_generator.no_entities_found'))
);
}
}
return $this->render('LabelSystem/dialog.html.twig', [
'form' => $form->createView(),
'pdf_data' => $pdf_data,
'filename' => $filename,
'profile' => $profile,
]);
}
protected function getLabelName(AbstractDBElement $element, ?LabelProfile $profile = null): string
{
$ret = 'label_' . $this->elementTypeNameGenerator->getLocalizedTypeLabel($element);
$ret .= $element->getID();
return $ret . '.pdf';
}
protected function findObjects(string $type, string $ids): array
{
if(!isset(LabelGenerator::CLASS_SUPPORT_MAPPING[$type])) {
throw new \InvalidArgumentException('The given type is not known and can not be mapped to a class!');
}
$id_array = $this->rangeParser->parse($ids);
/** @var DBElementRepository $repo */
$repo = $this->em->getRepository(LabelGenerator::CLASS_SUPPORT_MAPPING[$type]);
return $repo->getElementsFromIDArray($id_array);
}
}

View file

@ -0,0 +1,93 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Controller;
use App\Form\LabelSystem\ScanDialogType;
use App\Services\LabelSystem\Barcodes\BarcodeRedirector;
use App\Services\LabelSystem\Barcodes\BarcodeNormalizer;
use Doctrine\ORM\EntityNotFoundException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormError;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/scan")
* @package App\Controller
*/
class ScanController extends AbstractController
{
protected $barcodeParser;
protected $barcodeNormalizer;
public function __construct(BarcodeRedirector $barcodeParser, BarcodeNormalizer $barcodeNormalizer)
{
$this->barcodeParser = $barcodeParser;
$this->barcodeNormalizer = $barcodeNormalizer;
}
/**
* @Route("", name="scan_dialog")
*/
public function dialog(Request $request): Response
{
$this->denyAccessUnlessGranted('@tools.label_scanner');
$form = $this->createForm(ScanDialogType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$input = $form['input']->getData();
try {
[$type, $id] = $this->barcodeNormalizer->normalizeBarcodeContent($input);
try {
return $this->redirect($this->barcodeParser->getRedirectURL($type, $id));
} catch (EntityNotFoundException $exception) {
$this->addFlash('success', 'scan.qr_not_found');
}
} catch (\InvalidArgumentException $exception) {
$this->addFlash('error', 'scan.format_unknown');
}
}
return $this->render('LabelSystem/Scanner/dialog.html.twig', [
'form' => $form->createView(),
]);
}
/**
* The route definition for this action is done in routes.yaml, as it does not use the _locale prefix as the other routes
* @param string $type
* @param int $id
*/
public function scanQRCode(string $type, int $id): Response
{
try {
$this->addFlash('success', 'scan.qr_success');
return $this->redirect($this->barcodeParser->getRedirectURL($type, $id));
} catch (EntityNotFoundException $exception) {
$this->addFlash('success', 'scan.qr_not_found');
return $this->redirectToRoute('homepage');
}
}
}

View file

@ -82,8 +82,8 @@ class GroupFixtures extends Fixture
'suppliers' => 5461,
'manufacturers' => 5461,
'attachment_types' => 1365,
'tools' => 1365,
'labels' => 21,
'tools' => 87381,
'labels' => 87381,
'parts_category' => 5,
'parts_minamount' => 5,
'parts_lots' => 85,
@ -125,8 +125,8 @@ class GroupFixtures extends Fixture
'suppliers' => 1705,
'manufacturers' => 1705,
'attachment_types' => 681,
'tools' => 1366,
'labels' => 165,
'tools' => 87382,
'labels' => 173737,
'parts_category' => 9,
'parts_minamount' => 9,
'parts_lots' => 169,
@ -168,8 +168,8 @@ class GroupFixtures extends Fixture
'suppliers' => 5461,
'manufacturers' => 5461,
'attachment_types' => 1365,
'tools' => 1365,
'labels' => 85,
'tools' => 87381,
'labels' => 91477,
'parts_category' => 5,
'parts_minamount' => 5,
'parts_lots' => 85,

View file

@ -0,0 +1,96 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\DataFixtures;
use App\Entity\LabelSystem\LabelOptions;
use App\Entity\LabelSystem\LabelProfile;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\ObjectManager;
class LabelProfileFixtures extends Fixture
{
protected $em;
public function __construct(EntityManagerInterface $entityManager)
{
$this->em = $entityManager;
}
public function load(ObjectManager $manager)
{
$this->em->getConnection()->exec("ALTER TABLE `label_profiles` AUTO_INCREMENT = 1;");
$profile1 = new LabelProfile();
$profile1->setName('Profile 1');
$profile1->setShowInDropdown(true);
$option1 = new LabelOptions();
$option1->setLines("[[NAME]]\n[[DESCRIPION]]");
$option1->setBarcodeType('none');
$option1->setSupportedElement('part');
$profile1->setOptions($option1);
$manager->persist($profile1);
$profile2 = new LabelProfile();
$profile2->setName('Profile 2');
$profile2->setShowInDropdown(false);
$option2 = new LabelOptions();
$option2->setLines("[[NAME]]\n[[DESCRIPION]]");
$option2->setBarcodeType('qr');
$option2->setSupportedElement('part');
$profile2->setOptions($option2);
$manager->persist($profile2);
$profile3 = new LabelProfile();
$profile3->setName('Profile 3');
$profile3->setShowInDropdown(true);
$option3 = new LabelOptions();
$option3->setLines("[[NAME]]\n[[DESCRIPION]]");
$option3->setBarcodeType('code128');
$option3->setSupportedElement('part_lot');
$profile3->setOptions($option3);
$manager->persist($profile3);
$profile4 = new LabelProfile();
$profile4->setName('Profile 4');
$profile4->setShowInDropdown(true);
$option4 = new LabelOptions();
$option4->setLines("{{ element.name }}");
$option4->setBarcodeType('code39');
$option4->setSupportedElement('part');
$option4->setLinesMode('twig');
$profile4->setOptions($option4);
$manager->persist($profile4);
$manager->flush();
}
}

View file

@ -43,7 +43,7 @@ use LogicException;
* "Footprint" = "FootprintAttachment", "Manufacturer" = "ManufacturerAttachment",
* "Currency" = "CurrencyAttachment", "Group" = "GroupAttachment",
* "MeasurementUnit" = "MeasurementUnitAttachment", "Storelocation" = "StorelocationAttachment",
* "Supplier" = "SupplierAttachment", "User" = "UserAttachment"
* "Supplier" = "SupplierAttachment", "User" = "UserAttachment", "LabelProfile" = "LabelAttachment",
* })
* @ORM\EntityListeners({"App\EntityListeners\AttachmentDeleteListener"})
*/

View file

@ -0,0 +1,44 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Entity\Attachments;
use App\Entity\LabelSystem\LabelProfile;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
/**
* A attachment attached to a user element.
*
* @ORM\Entity()
* @UniqueEntity({"name", "attachment_type", "element"})
*/
class LabelAttachment extends Attachment
{
public const ALLOWED_ELEMENT_CLASS = LabelProfile::class;
/**
* @var LabelProfile the element this attachment is associated with
* @ORM\ManyToOne(targetEntity="App\Entity\LabelSystem\LabelProfile", inversedBy="attachments")
* @ORM\JoinColumn(name="element_id", referencedColumnName="id", nullable=false, onDelete="CASCADE").
*/
protected $element;
}

View file

@ -0,0 +1,240 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Entity\LabelSystem;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Embeddable()
*/
class LabelOptions
{
public const BARCODE_TYPES = ['none', /*'ean8',*/ 'qr', 'code39', 'datamatrix', 'code93', 'code128'];
public const SUPPORTED_ELEMENTS = ['part', 'part_lot', 'storelocation'];
public const PICTURE_TYPES = ['none', 'element_picture', 'main_attachment'];
public const LINES_MODES = ['html', 'twig'];
/**
* @var float The page size of the label in mm
* @Assert\Positive()
* @ORM\Column(type="float")
*/
protected $width = 50.0;
/**
* @var float The page size of the label in mm
* @Assert\Positive()
* @ORM\Column(type="float")
*/
protected $height = 30.0;
/**
* @var string The type of the barcode that should be used in the label (e.g. 'qr')
* @Assert\Choice(choices=LabelOptions::BARCODE_TYPES)
* @ORM\Column(type="string")
*/
protected $barcode_type = 'none';
/**
* @var string What image should be shown along the
* @Assert\Choice(choices=LabelOptions::PICTURE_TYPES)
* @ORM\Column(type="string")
*/
protected $picture_type = 'none';
/**
* @var string
* @Assert\Choice(choices=LabelOptions::SUPPORTED_ELEMENTS)
* @ORM\Column(type="string")
*/
protected $supported_element = 'part';
/**
* @var string Any additional CSS for the label.
* @ORM\Column(type="text")
*/
protected $additional_css = '';
/** @var string The mode that will be used to interpret the lines.
* @Assert\Choice(choices=LabelOptions::LINES_MODES)
* @ORM\Column(type="string")
*/
protected $lines_mode = 'html';
/**
* @var string
* @ORM\Column(type="text")
*/
protected $lines = '';
/**
* @return float
*/
public function getWidth(): float
{
return $this->width;
}
/**
* @param float $width
* @return LabelOptions
*/
public function setWidth(float $width): LabelOptions
{
$this->width = $width;
return $this;
}
/**
* @return float
*/
public function getHeight(): float
{
return $this->height;
}
/**
* @param float $height
* @return LabelOptions
*/
public function setHeight(float $height): LabelOptions
{
$this->height = $height;
return $this;
}
/**
* @return string
*/
public function getBarcodeType(): string
{
return $this->barcode_type;
}
/**
* @param string $barcode_type
* @return LabelOptions
*/
public function setBarcodeType(string $barcode_type): LabelOptions
{
$this->barcode_type = $barcode_type;
return $this;
}
/**
* @return string
*/
public function getPictureType(): string
{
return $this->picture_type;
}
/**
* @param string $picture_type
* @return LabelOptions
*/
public function setPictureType(string $picture_type): LabelOptions
{
$this->picture_type = $picture_type;
return $this;
}
/**
* @return string
*/
public function getSupportedElement(): string
{
return $this->supported_element;
}
/**
* @param string $supported_element
* @return LabelOptions
*/
public function setSupportedElement(string $supported_element): LabelOptions
{
$this->supported_element = $supported_element;
return $this;
}
/**
* @return string
*/
public function getLines(): string
{
return $this->lines;
}
/**
* @param string $lines
* @return LabelOptions
*/
public function setLines(string $lines): LabelOptions
{
$this->lines = $lines;
return $this;
}
/**
* Gets additional CSS (it will simply be attached
* @return string
*/
public function getAdditionalCss(): string
{
return $this->additional_css;
}
/**
*
* @param string $additional_css
* @return LabelOptions
*/
public function setAdditionalCss(string $additional_css): LabelOptions
{
$this->additional_css = $additional_css;
return $this;
}
/**
* @return string
*/
public function getLinesMode(): string
{
return $this->lines_mode;
}
/**
* @param string $lines_mode
* @return LabelOptions
*/
public function setLinesMode(string $lines_mode): LabelOptions
{
$this->lines_mode = $lines_mode;
return $this;
}
}

View file

@ -0,0 +1,126 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Entity\LabelSystem;
use App\Entity\Attachments\AttachmentContainingDBElement;
use App\Entity\Attachments\LabelAttachment;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity(repositoryClass="App\Repository\LabelProfileRepository")
* @ORM\Table(name="label_profiles")
* @ORM\EntityListeners({"App\EntityListeners\TreeCacheInvalidationListener"})
* @UniqueEntity({"name", "options.supported_element"})
* @package App\Entity\LabelSystem
*/
class LabelProfile extends AttachmentContainingDBElement
{
/**
* @var Collection<int, LabelAttachment>
* @ORM\OneToMany(targetEntity="App\Entity\Attachments\LabelAttachment", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true)
* @ORM\OrderBy({"name" = "ASC"})
*/
protected $attachments;
/**
* @var LabelOptions
* @ORM\Embedded(class="LabelOptions")
* @Assert\Valid()
*/
protected $options;
/**
* @var string The comment info for this element
* @ORM\Column(type="text")
*/
protected $comment = '';
/**
* @var bool Determines, if this label profile should be shown in the dropdown quick menu.
* @ORM\Column(type="boolean")
*/
protected $show_in_dropdown = true;
public function __construct()
{
parent::__construct();
$this->options = new LabelOptions();
}
public function getOptions(): LabelOptions
{
return $this->options;
}
public function setOptions(LabelOptions $labelOptions): LabelProfile
{
$this->options = $labelOptions;
return $this;
}
/**
* Get the comment of the element.
* @return string the comment
*/
public function getComment(): ?string
{
return $this->comment;
}
public function setComment(string $new_comment): string
{
$this->comment = $new_comment;
return $this;
}
/**
* Returns true, if this label profile should be shown in label generator quick menu.
* @return bool
*/
public function isShowInDropdown(): bool
{
return $this->show_in_dropdown;
}
/**
* Sets the show in dropdown menu.
* @param bool $show_in_dropdown
* @return LabelProfile
*/
public function setShowInDropdown(bool $show_in_dropdown): LabelProfile
{
$this->show_in_dropdown = $show_in_dropdown;
return $this;
}
/**
* @inheritDoc
*/
public function getIDString(): string
{
return 'LP'.sprintf('%09d', $this->getID());
}
}

View file

@ -47,6 +47,7 @@ use App\Entity\Attachments\AttachmentType;
use App\Entity\Base\AbstractDBElement;
use App\Entity\Devices\Device;
use App\Entity\Devices\DevicePart;
use App\Entity\LabelSystem\LabelProfile;
use App\Entity\Parameters\AbstractParameter;
use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint;
@ -117,6 +118,7 @@ abstract class AbstractLogEntry extends AbstractDBElement
protected const TARGET_TYPE_PRICEDETAIL = 16;
protected const TARGET_TYPE_MEASUREMENTUNIT = 17;
protected const TARGET_TYPE_PARAMETER = 18;
protected const TARGET_TYPE_LABEL_PROFILE = 19;
/** @var array This const is used to convert the numeric level to a PSR-3 compatible log level */
protected const LEVEL_ID_TO_STRING = [
@ -149,6 +151,7 @@ abstract class AbstractLogEntry extends AbstractDBElement
self::TARGET_TYPE_PRICEDETAIL => Pricedetail::class,
self::TARGET_TYPE_MEASUREMENTUNIT => MeasurementUnit::class,
self::TARGET_TYPE_PARAMETER => AbstractParameter::class,
self::TARGET_TYPE_LABEL_PROFILE => LabelProfile::class,
];
/** @var User The user which has caused this log entry
@ -163,7 +166,7 @@ abstract class AbstractLogEntry extends AbstractDBElement
protected $timestamp;
/** @var int The priority level of the associated level. 0 is highest, 7 lowest
* @ORM\Column(type="integer", name="level", columnDefinition="TINYINT")
* @ORM\Column(type="integer", name="level", columnDefinition="TINYINT(4) NOT NULL")
*/
protected $level;

View file

@ -44,6 +44,7 @@ namespace App\EntityListeners;
use App\Entity\Base\AbstractDBElement;
use App\Entity\Base\AbstractStructuralDBElement;
use App\Entity\LabelSystem\LabelProfile;
use App\Entity\UserSystem\Group;
use App\Entity\UserSystem\User;
use App\Services\UserCacheKeyGenerator;
@ -71,7 +72,7 @@ class TreeCacheInvalidationListener
public function invalidate(AbstractDBElement $element, LifecycleEventArgs $event): void
{
//If an element was changed, then invalidate all cached trees with this element class
if ($element instanceof AbstractStructuralDBElement) {
if ($element instanceof AbstractStructuralDBElement || $element instanceof LabelProfile) {
$secure_class_name = str_replace('\\', '_', get_class($element));
$this->cache->invalidateTags([$secure_class_name]);
}

View file

@ -0,0 +1,33 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Exceptions;
use Throwable;
use Twig\Error\Error;
class TwigModeException extends \RuntimeException
{
public function __construct(Error $previous = null)
{
parent::__construct($previous->getMessage(), 0, $previous);
}
}

View file

@ -45,6 +45,8 @@ namespace App\Form\AdminPages;
use App\Entity\Attachments\Attachment;
use App\Entity\Base\AbstractNamedDBElement;
use App\Entity\Base\AbstractStructuralDBElement;
use App\Entity\LabelSystem\LabelOptions;
use App\Entity\LabelSystem\LabelProfile;
use App\Form\AttachmentFormType;
use App\Form\ParameterType;
use App\Form\Type\MasterPictureAttachmentType;
@ -94,26 +96,38 @@ class BaseEntityAdminForm extends AbstractType
'placeholder' => 'part.name.placeholder',
],
'disabled' => ! $this->security->isGranted($is_new ? 'create' : 'edit', $entity),
])
]);
->add('parent', StructuralEntityType::class, [
if ($entity instanceof AbstractStructuralDBElement) {
$builder->add(
'parent',
StructuralEntityType::class,
[
'class' => get_class($entity),
'required' => false,
'label' => 'parent.label',
'disabled' => ! $this->security->isGranted($is_new ? 'create' : 'move', $entity),
])
->add('not_selectable', CheckboxType::class, [
'disabled' => !$this->security->isGranted($is_new ? 'create' : 'move', $entity),
]
)
->add(
'not_selectable',
CheckboxType::class,
[
'required' => false,
'label' => 'entity.edit.not_selectable',
'help' => 'entity.edit.not_selectable.help',
'label_attr' => [
'class' => 'checkbox-custom',
],
'disabled' => ! $this->security->isGranted($is_new ? 'create' : 'edit', $entity),
])
->add('comment', CKEditorType::class, [
'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity),
]
);
}
if ($entity instanceof AbstractStructuralDBElement || $entity instanceof LabelProfile) {
$builder->add(
'comment',
CKEditorType::class,
[
'required' => false,
'empty_data' => '',
'label' => 'comment.label',
@ -121,8 +135,10 @@ class BaseEntityAdminForm extends AbstractType
'rows' => 4,
],
'help' => 'bbcode.hint',
'disabled' => ! $this->security->isGranted($is_new ? 'create' : 'edit', $entity),
]);
'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity),
]
);
}
$this->additionalFormElements($builder, $options, $entity);
@ -154,11 +170,15 @@ class BaseEntityAdminForm extends AbstractType
'empty_data' => null,
]);
$builder->add('parameters', CollectionType::class, [
if ($entity instanceof AbstractStructuralDBElement) {
$builder->add(
'parameters',
CollectionType::class,
[
'entry_type' => ParameterType::class,
'allow_add' => $this->security->isGranted($is_new ? 'create' : 'edit', $entity),
'allow_delete' => $this->security->isGranted($is_new ? 'create' : 'edit', $entity),
'disabled' => ! $this->security->isGranted($is_new ? 'create' : 'edit', $entity),
'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity),
'reindex_enable' => true,
'label' => false,
'by_reference' => false,
@ -166,7 +186,9 @@ class BaseEntityAdminForm extends AbstractType
'entry_options' => [
'data_class' => $options['parameter_class'],
],
]);
]
);
}
//Buttons
$builder->add('save', SubmitType::class, [

View file

@ -0,0 +1,55 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Form\AdminPages;
use App\Entity\Base\AbstractNamedDBElement;
use App\Entity\LabelSystem\LabelProfile;
use App\Form\LabelOptionsType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class LabelProfileAdminForm extends BaseEntityAdminForm
{
protected function additionalFormElements(FormBuilderInterface $builder, array $options, AbstractNamedDBElement $entity): void
{
$builder->add('show_in_dropdown', CheckboxType::class, [
'required' => false,
'label' => 'label_profile.showInDropdown',
'label_attr' => [
'class' => 'checkbox-custom',
],
]);
$builder->add('options', LabelOptionsType::class, [
'label' => false,
'disabled' => $options['disable_options'],
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
parent::configureOptions($resolver);
$resolver->setDefault('data_class', LabelProfile::class);
$resolver->setDefault('disable_options', false);
}
}

View file

@ -0,0 +1,142 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Form;
use App\Entity\LabelSystem\LabelOptions;
use FOS\CKEditorBundle\Form\Type\CKEditorType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\Security;
class LabelOptionsType extends AbstractType
{
private $security;
public function __construct(Security $security)
{
$this->security = $security;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('width', NumberType::class, [
'label' => 'label_options.page_size.label',
'html5' => true,
'attr' => [
'placeholder' => 'label_options.width.placeholder',
'min' => 0,
'step' => 'any',
]
]);
$builder->add('height', NumberType::class, [
'label' => false,
'html5' => true,
'attr' => [
'placeholder' => 'label_options.height.placeholder',
'min' => 0,
'step' => 'any',
]
]);
$builder->add('supported_element', ChoiceType::class, [
'label' => 'label_options.supported_elements.label',
'choices' => [
'part.label' => 'part',
'part_lot.label' => 'part_lot',
'storelocation.label' => 'storelocation',
]
]);
$builder->add('barcode_type', ChoiceType::class, [
'label' => 'label_options.barcode_type.label',
'empty_data' => 'none',
'choices' => [
'label_options.barcode_type.none' => 'none',
'label_options.barcode_type.qr' => 'qr',
'label_options.barcode_type.code128' => 'code128',
'label_options.barcode_type.code39' => 'code39',
'label_options.barcode_type.code93' => 'code93',
'label_options.barcode_type.datamatrix' => 'datamatrix',
],
'group_by' => function ($choice, $key, $value) {
if (in_array($choice, ['qr', 'datamatrix'])) {
return 'label_options.barcode_type.2D';
}
if (in_array($choice, ['code39', 'code93', 'code128'])) {
return 'label_options.barcode_type.1D';
}
return null;
},
'attr' => [
'class' => 'selectpicker',
'data-live-search' => true,
],
]);
$builder->add('lines', CKEditorType::class, [
'label' => 'label_profile.lines.label',
'empty_data' => '',
'attr' => [
'rows' => 4,
],
'config_name' => 'label_config',
]);
$builder->add('additional_css', TextareaType::class, [
'label' => 'label_options.additional_css.label',
'empty_data' => '',
'attr' => [
'rows' => 4,
],
'required' => false,
]);
$builder->add('lines_mode', ChoiceType::class, [
'label' => 'label_options.lines_mode.label',
'choices' => [
'label_options.lines_mode.html' => 'html',
'label.options.lines_mode.twig' => 'twig',
],
'help' => 'label_options.lines_mode.help',
'help_html' => true,
'expanded' => true,
'attr' => [
'class' => 'pt-2'
],
'label_attr' => [
'class' => 'radio-custom radio-inline'
],
'disabled' => !$this->security->isGranted('@labels.use_twig')
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('data_class', LabelOptions::class);
}
}

View file

@ -0,0 +1,70 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Form\LabelSystem;
use App\Form\LabelOptionsType;
use App\Validator\Constraints\Misc\ValidRange;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\Security;
class LabelDialogType extends AbstractType
{
protected $security;
public function __construct(Security $security)
{
$this->security = $security;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('target_id', TextType::class, [
'required' => true,
'label' => 'label_generator.target_id.label',
'help' => 'label_generator.target_id.range_hint',
'constraints' => [
new ValidRange(),
],
]);
$builder->add('options', LabelOptionsType::class, [
'label' => false,
'disabled' => !$this->security->isGranted('@labels.edit_options') || $options['disable_options'],
]);
$builder->add('update', SubmitType::class, [
'label' => 'label_generator.update',
]);
}
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefault('mapped', false);
$resolver->setDefault('disable_options', false);
}
}

View file

@ -0,0 +1,51 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Form\LabelSystem;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ScanDialogType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('input', TextType::class, [
'label' => 'scan_dialog.input',
'attr' => [
'autofocus' => true,
'id' => 'scan_dialog_input',
],
]);
$builder->add('submit', SubmitType::class, [
'label' => 'scan_dialog.submit'
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('mapped', false);
}
}

View file

@ -178,6 +178,7 @@ class UserAdminForm extends AbstractType
'attr' => [
'class' => 'selectpicker',
],
'choice_translation_domain' => false,
'placeholder' => 'user_settings.theme.placeholder',
'label' => 'user.theme.label',
'disabled' => ! $this->security->isGranted('change_user_settings', $entity),

View file

@ -0,0 +1,112 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Helpers;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class LabelResponse extends Response
{
public function __construct($content = '', int $status = 200, array $headers = [])
{
parent::__construct($content, $status, $headers);
}
public function setContent($content)
{
parent::setContent($content);
$this->setAutoEtag();
$this->setAutoLastModified();
return $this;
}
public function prepare(Request $request)
{
parent::prepare($request);
$this->headers->set('Content-Type','application/pdf');
if ('HTTP/1.0' !== $request->server->get('SERVER_PROTOCOL')) {
$this->setProtocolVersion('1.1');
}
$this->ensureIEOverSSLCompatibility($request);
return $this;
}
/**
* Automatically sets the Last-Modified header according the file modification date.
*/
public function setAutoLastModified()
{
$this->setLastModified(new \DateTime());
return $this;
}
/**
* Automatically sets the ETag header according to the checksum of the file.
*/
public function setAutoEtag()
{
$this->setEtag(base64_encode(hash('sha256', $this->content, true)));
return $this;
}
/**
* Sets the Content-Disposition header with the given filename.
*
* @param string $disposition ResponseHeaderBag::DISPOSITION_INLINE or ResponseHeaderBag::DISPOSITION_ATTACHMENT
* @param string $filename Optionally use this UTF-8 encoded filename instead of the real name of the file
* @param string $filenameFallback A fallback filename, containing only ASCII characters. Defaults to an automatically encoded filename
*
* @return $this
*/
public function setContentDisposition($disposition, $filename, $filenameFallback = '')
{
if ('' === $filenameFallback && (!preg_match('/^[\x20-\x7e]*$/', $filename) || false !== strpos($filename, '%'))) {
$encoding = mb_detect_encoding($filename, null, true) ?: '8bit';
for ($i = 0, $filenameLength = mb_strlen($filename, $encoding); $i < $filenameLength; ++$i) {
$char = mb_substr($filename, $i, 1, $encoding);
if ('%' === $char || \ord($char) < 32 || \ord($char) > 126) {
$filenameFallback .= '_';
} else {
$filenameFallback .= $char;
}
}
}
$dispositionHeader = $this->headers->makeDisposition($disposition, $filename, $filenameFallback);
$this->headers->set('Content-Disposition', $dispositionHeader);
return $this;
}
}

View file

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20200502161750 extends AbstractMigration
{
public function getDescription() : string
{
return '';
}
public function up(Schema $schema) : void
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('CREATE TABLE label_profiles (id INT AUTO_INCREMENT NOT NULL, id_preview_attachement INT DEFAULT NULL, comment LONGTEXT NOT NULL, show_in_dropdown TINYINT(1) NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, options_width DOUBLE PRECISION NOT NULL, options_height DOUBLE PRECISION NOT NULL, options_barcode_type VARCHAR(255) NOT NULL, options_picture_type VARCHAR(255) NOT NULL, options_supported_element VARCHAR(255) NOT NULL, options_additional_css LONGTEXT NOT NULL, options_lines_mode VARCHAR(255) NOT NULL, options_lines LONGTEXT NOT NULL, INDEX IDX_C93E9CF56DEDCEC2 (id_preview_attachement), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
$this->addSql('ALTER TABLE label_profiles ADD CONSTRAINT FK_C93E9CF56DEDCEC2 FOREIGN KEY (id_preview_attachement) REFERENCES `attachments` (id)');
$this->addSql('ALTER TABLE log CHANGE level level TINYINT(4) NOT NULL');
}
public function down(Schema $schema) : void
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('DROP TABLE label_profiles');
$this->addSql('ALTER TABLE log CHANGE level level TINYINT(1) DEFAULT NULL');
}
}

View file

@ -50,6 +50,22 @@ class DBElementRepository extends EntityRepository
$this->setField($element, 'id', $new_id);
}
/**
* Find all elements that match a list of IDs.
* @param array $ids
* @return AbstractDBElement[]
*/
public function getElementsFromIDArray(array $ids): array
{
$qb = $this->createQueryBuilder('element');
$q = $qb->select('element')
->where('element.id IN (?1)')
->setParameter(1, $ids)
->getQuery();
return $q->getResult();
}
protected function setField(AbstractDBElement $element, string $field, int $new_value): void
{
$reflection = new \ReflectionClass(get_class($element));

View file

@ -0,0 +1,89 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Repository;
use App\Entity\LabelSystem\LabelOptions;
use App\Entity\LabelSystem\LabelProfile;
use App\Helpers\Trees\TreeViewNode;
use Symfony\Contracts\Translation\TranslatorInterface;
class LabelProfileRepository extends NamedDBElementRepository
{
/**
* Find the profiles that are shown in the dropdown for the given type.
* You should maybe use the cached version of this in LabelProfileDropdownHelper
* @param string $type
* @return array
*/
public function getDropdownProfiles(string $type): array
{
if (!in_array($type, LabelOptions::SUPPORTED_ELEMENTS)) {
throw new \InvalidArgumentException('Invalid supported_element type given.');
}
return $this->findBy(['options.supported_element' => $type, 'show_in_dropdown' => true], ['name' => 'ASC']);
}
/**
* Gets a tree of TreeViewNode elements. The root elements has $parent as parent.
* The treeview is generic, that means the href are null and ID values are set.
*
* @return TreeViewNode[]
*/
public function getGenericNodeTree(): array
{
$result = [];
foreach (LabelOptions::SUPPORTED_ELEMENTS as $type) {
$type_children = [];
$entities = $this->findForSupportedElement($type);
foreach ($entities as $entity) {
/** @var LabelProfile $entity */
$node = new TreeViewNode($entity->getName(), null, null);
$node->setId($entity->getID());
$type_children[] = $node;
}
if (!empty($type_children)) {
//Use default label e.g. 'part_label'. $$ marks that it will be translated in TreeViewGenerator
$tmp = new TreeViewNode('$$' . $type . '.label', null, $type_children);
$result[] = $tmp;
}
}
return $result;
}
/**
* Find all LabelProfiles that can be used with the given type
* @param string $type See LabelOptions::SUPPORTED_ELEMENTS for valid values.
* @param array $order_by The way the results should be sorted. By default ordered by
* @return array
*/
public function findForSupportedElement(string $type, array $order_by = ['name' => 'ASC']): array
{
if (!in_array($type, LabelOptions::SUPPORTED_ELEMENTS)) {
throw new \InvalidArgumentException('Invalid supported_element type given.');
}
return $this->findBy(['options.supported_element' => $type], $order_by);
}
}

View file

@ -0,0 +1,62 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Security\Voter;
use App\Entity\LabelSystem\LabelProfile;
use App\Entity\UserSystem\User;
class LabelProfileVoter extends ExtendedVoter
{
protected const MAPPING = [
'read' => 'read_profiles',
'create' => 'create_profiles',
'edit' => 'edit_profiles',
'delete' => 'delete_profiles',
'show_history' => 'show_history',
'revert_element' => 'revert_element',
];
/**
* @inheritDoc
*/
protected function voteOnUser($attribute, $subject, User $user): bool
{
return $this->resolver->inherit($user, 'labels', self::MAPPING[$attribute]) ?? false;
}
/**
* @inheritDoc
*/
protected function supports($attribute, $subject)
{
if ($subject instanceof LabelProfile) {
if (!isset(self::MAPPING[$attribute])) {
return false;
}
return $this->resolver->isValidOperation('labels', self::MAPPING[$attribute]);
}
return false;
}
}

View file

@ -46,6 +46,7 @@ use App\Entity\Attachments\Attachment;
use App\Entity\Attachments\AttachmentType;
use App\Entity\Contracts\NamedElementInterface;
use App\Entity\Devices\Device;
use App\Entity\LabelSystem\LabelProfile;
use App\Entity\Parameters\AbstractParameter;
use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint;
@ -92,6 +93,7 @@ class ElementTypeNameGenerator
Group::class => $this->translator->trans('group.label'),
User::class => $this->translator->trans('user.label'),
AbstractParameter::class => $this->translator->trans('parameter.label'),
LabelProfile::class => $this->translator->trans('label_profile.label'),
];
}

View file

@ -47,6 +47,7 @@ use App\Entity\Attachments\AttachmentType;
use App\Entity\Attachments\PartAttachment;
use App\Entity\Base\AbstractDBElement;
use App\Entity\Devices\Device;
use App\Entity\LabelSystem\LabelProfile;
use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint;
use App\Entity\Parts\Manufacturer;
@ -133,7 +134,6 @@ class EntityURLGenerator
{
$map = [
Part::class => 'part_info',
//As long we does not have own things for it use edit page
AttachmentType::class => 'attachment_type_edit',
Category::class => 'category_edit',
@ -146,6 +146,7 @@ class EntityURLGenerator
Currency::class => 'currency_edit',
MeasurementUnit::class => 'measurement_unit_edit',
Group::class => 'group_edit',
LabelProfile::class => 'label_profile_edit',
];
try {
@ -241,6 +242,7 @@ class EntityURLGenerator
Currency::class => 'currency_edit',
MeasurementUnit::class => 'measurement_unit_edit',
Group::class => 'group_edit',
LabelProfile::class => 'label_profile_edit'
];
return $this->urlGenerator->generate($this->mapToController($map, $entity), ['id' => $entity->getID()]);
@ -270,6 +272,7 @@ class EntityURLGenerator
Currency::class => 'currency_edit',
MeasurementUnit::class => 'measurement_unit_edit',
Group::class => 'group_edit',
LabelProfile::class => 'label_profile_edit'
];
return $this->urlGenerator->generate($this->mapToController($map, $entity), ['id' => $entity->getID()]);
@ -299,6 +302,7 @@ class EntityURLGenerator
Currency::class => 'currency_new',
MeasurementUnit::class => 'measurement_unit_new',
Group::class => 'group_new',
LabelProfile::class => 'label_profile_new'
];
return $this->urlGenerator->generate($this->mapToController($map, $entity));
@ -329,6 +333,7 @@ class EntityURLGenerator
Currency::class => 'currency_clone',
MeasurementUnit::class => 'measurement_unit_clone',
Group::class => 'group_clone',
LabelProfile::class => 'label_profile_clone'
];
return $this->urlGenerator->generate($this->mapToController($map, $entity), ['id' => $entity->getID()]);
@ -371,6 +376,7 @@ class EntityURLGenerator
Currency::class => 'currency_delete',
MeasurementUnit::class => 'measurement_unit_delete',
Group::class => 'group_delete',
LabelProfile::class => 'label_profile_delete'
];
return $this->urlGenerator->generate($this->mapToController($map, $entity), ['id' => $entity->getID()]);

View file

@ -0,0 +1,84 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Services\LabelSystem;
use App\Entity\LabelSystem\LabelOptions;
use App\Services\LabelSystem\Barcodes\BarcodeContentGenerator;
use Com\Tecnick\Barcode\Barcode;
final class BarcodeGenerator
{
private $barcodeContentGenerator;
public function __construct(BarcodeContentGenerator $barcodeContentGenerator)
{
$this->barcodeContentGenerator = $barcodeContentGenerator;
}
public function generateSVG(LabelOptions $options, object $target): ?string
{
$barcode = new Barcode();
switch ($options->getBarcodeType()) {
case 'qr':
$type = 'QRCODE';
break;
case 'datamatrix':
$type = 'DATAMATRIX';
break;
case 'code39':
$type = 'C39';
break;
case 'code93':
$type = 'C93';
break;
case 'code128':
$type = 'C128A';
break;
case 'none':
return null;
default:
throw new \InvalidArgumentException('Unknown label type!');
}
$bobj = $barcode->getBarcodeObj($type, $this->getContent($options, $target));
return $bobj->getSvgCode();
}
public function getContent(LabelOptions $options, object $target): ?string
{
switch ($options->getBarcodeType()) {
case 'qr':
case 'datamatrix':
return $this->barcodeContentGenerator->getURLContent($target);
case 'code39':
case 'code93':
case 'code128':
return $this->barcodeContentGenerator->get1DBarcodeContent($target);
case 'none':
return null;
default:
throw new \InvalidArgumentException('Unknown label type!');
}
}
}

View file

@ -0,0 +1,98 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Services\LabelSystem\Barcodes;
use App\Entity\Base\AbstractDBElement;
use App\Entity\Parts\Manufacturer;
use App\Entity\Parts\Part;
use App\Entity\Parts\PartLot;
use App\Entity\Parts\Storelocation;
use App\Entity\Parts\Supplier;
use App\Exceptions\EntityNotSupportedException;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
final class BarcodeContentGenerator
{
private $urlGenerator;
public const PREFIX_MAP = [
Part::class => 'P',
PartLot::class => 'L',
Storelocation::class => 'S'
];
private const URL_MAP = [
Part::class => 'part',
PartLot::class => 'lot',
Storelocation::class => 'location',
];
public function __construct(UrlGeneratorInterface $urlGenerator)
{
$this->urlGenerator = $urlGenerator;
}
/**
* Generates a fixed URL to the given Element that can be embedded in a 2D code (e.g. QR code).
* @param AbstractDBElement $target
* @return string
*/
public function getURLContent(AbstractDBElement $target): string
{
$type = $this->classToString(self::URL_MAP, $target);
return $this->urlGenerator->generate('scan_qr', [
'type' => $type,
'id' => $target->getID() ?? 0,
'_locale' => null,
], UrlGeneratorInterface::ABSOLUTE_URL);
}
/**
* Returns a Code that can be used in a 1D barcode.
* The return value has a format of "L0123"
* @param AbstractDBElement $target
* @return string
*/
public function get1DBarcodeContent(AbstractDBElement $target): string
{
$prefix = $this->classToString(self::PREFIX_MAP, $target);
$id = sprintf('%04d', $target->getID() ?? 0);
return $prefix . $id;
}
private function classToString(array $map, object $target): string
{
$class = get_class($target);
if (isset($map[$class])) {
return $map[$class];
}
foreach($map as $class => $string) {
if (is_a($target, $class)) {
return $string;
}
}
throw new \InvalidArgumentException('Unknown object class ' . get_class($target));
}
}

View file

@ -0,0 +1,118 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Services\LabelSystem\Barcodes;
use App\Entity\Base\AbstractStructuralDBElement;
use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint;
use App\Entity\Parts\Manufacturer;
use App\Entity\Parts\Part;
use App\Entity\Parts\PartLot;
use App\Entity\Parts\Storelocation;
final class BarcodeExampleElementsGenerator
{
public function getElement(string $type): object
{
switch ($type) {
case 'part':
return $this->getExamplePart();
case 'part_lot':
return $this->getExamplePartLot();
case 'storelocation':
return $this->getStorelocation();
default:
throw new \InvalidArgumentException('Unknown $type.');
}
}
private function getStorelocation(): Storelocation
{
$storelocation = new Storelocation();
$storelocation->setName('Location 1');
$storelocation->setComment('Example comment');
$storelocation->updatedTimestamps();
$parent = new Storelocation();
$parent->setName('Parent');
$storelocation->setParent($parent);
return $storelocation;
}
private function getStructuralData(string $class): AbstractStructuralDBElement
{
if (!is_a($class, AbstractStructuralDBElement::class, true)) {
throw new \InvalidArgumentException('$class must be an child of AbstractStructuralDBElement');
}
/** @var AbstractStructuralDBElement $parent */
$parent = new $class();
$parent->setName('Example');
/** @var AbstractStructuralDBElement $child */
$child = new $class();
$child->setName((new \ReflectionClass($class))->getShortName());
$child->setParent($parent);
return $child;
}
public function getExamplePart(): Part
{
$part = new Part();
$part->setName('Example Part');
$part->setDescription('<b>Part</b> description');
$part->setComment('<i>Part</i> comment');
$part->setCategory($this->getStructuralData(Category::class));
$part->setFootprint($this->getStructuralData(Footprint::class));
$part->setManufacturer($this->getStructuralData(Manufacturer::class));
$part->setMass(123.4);
$part->setManufacturerProductNumber('CUSTOM MPN');
$part->setTags('Tag1, Tag2, Tag3');
$part->setManufacturingStatus('active');
$part->updatedTimestamps();
$part->setFavorite(true);
$part->setMinAmount(100);
$part->setNeedsReview(true);
return $part;
}
public function getExamplePartLot(): PartLot
{
$lot = new PartLot();
$lot->setPart($this->getExamplePart());
$lot->setDescription('Example Lot');
$lot->setComment('Lot comment');
$lot->setExpirationDate(new \DateTime('+1 days'));
$lot->setStorageLocation($this->getStructuralData(Storelocation::class));
$lot->setAmount(123);
return $lot;
}
}

View file

@ -0,0 +1,86 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Services\LabelSystem\Barcodes;
final class BarcodeNormalizer
{
private const PREFIX_TYPE_MAP = [
'L' => 'lot',
'P' => 'part',
'S' => 'location',
];
/**
* Parses barcode content and normalizes it.
* Returns an array in the format ['part', 1]: First entry contains element type, second the ID of the element
* @param string $input
* @return array
*/
public function normalizeBarcodeContent(string $input): array
{
$input = trim($input);
$matches = [];
//Some scanner output '-' as ß, so replace it (ß is never used, so we can replace it safely)
$input = str_replace('ß', '-', $input);
//Extract parts from QR code's URL
if (preg_match('#^https?://.*/scan/(\w+)/(\d+)/?$#', $input, $matches)) {
return [$matches[1], (int) $matches[2]];
}
//New Code39 barcode use L0001 format
if (preg_match('#^([A-Z])(\d{4,})$#', $input, $matches)) {
$prefix = $matches[1];
$id = (int) $matches[2];
if (!isset(self::PREFIX_TYPE_MAP[$prefix])) {
throw new \InvalidArgumentException('Unknown prefix ' . $prefix);
}
return [self::PREFIX_TYPE_MAP[$prefix], $id];
}
//During development the L-000001 format was used
if (preg_match('#^(\w)-(\d{6,})$#', $input, $matches)) {
$prefix = $matches[1];
$id = (int) $matches[2];
if (!isset(self::PREFIX_TYPE_MAP[$prefix])) {
throw new \InvalidArgumentException('Unknown prefix ' . $prefix);
}
return [self::PREFIX_TYPE_MAP[$prefix], $id];
}
//Legacy Part-DB location labels used $L00336 format
if (preg_match('#^\$L(\d{5,})$#', $input, $matches)) {
return ['location', (int) $matches[1]];
}
//Legacy Part-DB used EAN8 barcodes for part labels. Format 0000001(2) (note the optional 8th digit => checksum)
if (preg_match('#^(\d{7})\d?$#', $input, $matches)) {
return ['part', (int) $matches[1]];
}
throw new \InvalidArgumentException('Unknown barcode format!');
}
}

View file

@ -0,0 +1,69 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Services\LabelSystem\Barcodes;
use App\Entity\Parts\PartLot;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityNotFoundException;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
final class BarcodeRedirector
{
private $urlGenerator;
private $em;
public function __construct(UrlGeneratorInterface $urlGenerator, EntityManagerInterface $entityManager)
{
$this->urlGenerator = $urlGenerator;
$this->em = $entityManager;
}
/**
* Determines the URL to which the user should be redirected, when scanning a QR code
* @param string $type The type of the element that was scanned (e.g. 'part', 'lot', etc.)
* @param int $id The ID of the element that was scanned
* @return string The URL to which should be redirected.
* @throws EntityNotFoundException
*/
public function getRedirectURL(string $type, int $id): string
{
switch ($type) {
case 'part':
return $this->urlGenerator->generate('app_part_show', ['id' => $id]);
case 'lot':
//Try to determine the part to the given lot
$lot = $this->em->find(PartLot::class, $id);
if ($lot === null) {
throw new EntityNotFoundException();
}
return $this->urlGenerator->generate('app_part_show', ['id' => $lot->getPart()->getID()]);
case 'location':
return $this->urlGenerator->generate('part_list_store_location', ['id' => $id]);
default:
throw new \InvalidArgumentException('Unknown $type: ' . $type);
}
}
}

View file

@ -0,0 +1,105 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Services\LabelSystem;
use App\Entity\Contracts\NamedElementInterface;
use App\Entity\LabelSystem\LabelOptions;
use App\Entity\Parts\Part;
use App\Entity\Parts\PartLot;
use App\Entity\Parts\Storelocation;
use App\Services\ElementTypeNameGenerator;
use Dompdf\Dompdf;
use Twig\Environment;
final class LabelGenerator
{
public const CLASS_SUPPORT_MAPPING = [
'part' => Part::class,
'part_lot' => PartLot::class,
'storelocation' => Storelocation::class,
];
public const MM_TO_POINTS_FACTOR = 2.83465;
private $labelHTMLGenerator;
public function __construct(LabelHTMLGenerator $labelHTMLGenerator)
{
$this->labelHTMLGenerator = $labelHTMLGenerator;
}
/**
* @param LabelOptions $options
* @param object|object[] $elements An element or an array of elements for which labels should be generated
* @return string
*/
public function generateLabel(LabelOptions $options, $elements): string
{
if (!is_array($elements) && !is_object($elements)) {
throw new \InvalidArgumentException('$element must be an object or an array of objects!');
}
if (!is_array($elements)) {
$elements = [$elements];
}
foreach ($elements as $element) {
if (!$this->supports($options, $element)) {
throw new \InvalidArgumentException('The given options are not compatible with the given element!');
}
}
$dompdf = new Dompdf();
$dompdf->setPaper($this->mmToPointsArray($options->getWidth(), $options->getHeight()));
$dompdf->loadHtml($this->labelHTMLGenerator->getLabelHTML($options, $elements));
$dompdf->render();
return $dompdf->output();
}
/**
* Check if the given LabelOptions can be used with $element.
* @param LabelOptions $options
* @param object $element
* @return bool
*/
public function supports(LabelOptions $options, object $element)
{
$supported_type = $options->getSupportedElement();
if (!isset(static::CLASS_SUPPORT_MAPPING[$supported_type])) {
throw new \InvalidArgumentException('Supported type name of the Label options not known!');
}
return is_a($element, static::CLASS_SUPPORT_MAPPING[$supported_type]);
}
/**
* Converts width and height given in mm to an size array, that can be used by DOMPDF for page size
* @param float $width The width of the paper
* @param float $height The height of the paper
* @return float[]
*/
public function mmToPointsArray(float $width, float $height): array
{
return [0.0, 0.0, $width * self::MM_TO_POINTS_FACTOR, $height * self::MM_TO_POINTS_FACTOR];
}
}

View file

@ -0,0 +1,115 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Services\LabelSystem;
use App\Entity\Contracts\NamedElementInterface;
use App\Entity\LabelSystem\LabelOptions;
use App\Exceptions\TwigModeException;
use App\Services\ElementTypeNameGenerator;
use App\Services\LabelSystem\Barcodes\BarcodeContentGenerator;
use Symfony\Component\Security\Core\Security;
use Twig\Environment;
use Twig\Error\Error;
use Twig\Error\SyntaxError;
final class LabelHTMLGenerator
{
private $twig;
private $elementTypeNameGenerator;
private $replacer;
private $barcodeGenerator;
private $sandboxedTwigProvider;
private $partdb_title;
private $security;
public function __construct(ElementTypeNameGenerator $elementTypeNameGenerator, LabelTextReplacer $replacer, Environment $twig,
BarcodeGenerator $barcodeGenerator, SandboxedTwigProvider $sandboxedTwigProvider, Security $security, string $partdb_title)
{
$this->twig = $twig;
$this->elementTypeNameGenerator = $elementTypeNameGenerator;
$this->replacer = $replacer;
$this->barcodeGenerator = $barcodeGenerator;
$this->sandboxedTwigProvider = $sandboxedTwigProvider;
$this->security = $security;
$this->partdb_title = $partdb_title;
}
public function getLabelHTML(LabelOptions $options, array $elements): string
{
if (empty($elements)) {
throw new \InvalidArgumentException('$elements must not be empty');
}
$twig_elements = [];
if ($options->getLinesMode() === 'twig') {
$sandboxed_twig = $this->sandboxedTwigProvider->getTwig($options);
$current_user = $this->security->getUser();
}
$page = 1;
foreach ($elements as $element) {
if ($options->getLinesMode() === 'twig' && isset($sandboxed_twig) && isset($current_user)) {
try {
$lines = $sandboxed_twig->render(
'lines',
[
'element' => $element,
'page' => $page,
'user' => $current_user,
'install_title' => $this->partdb_title,
]
);
} catch (Error $exception) {
throw new TwigModeException($exception);
}
} else {
$lines = $this->replacer->replace($options->getLines(), $element);
}
$twig_elements[] = [
'element' => $element,
'lines' => $lines,
'barcode' => $this->barcodeGenerator->generateSVG($options, $element),
'barcode_content' => $this->barcodeGenerator->getContent($options, $element),
];
$page++;
}
return $this->twig->render('LabelSystem/labels/base_label.html.twig', [
'meta_title' => $this->getPDFTitle($options, $elements[0]),
'elements' => $twig_elements,
'options' => $options,
]);
}
protected function getPDFTitle(LabelOptions $options, object $element)
{
if ($element instanceof NamedElementInterface) {
return $this->elementTypeNameGenerator->getTypeNameCombination($element, false);
}
return 'Part-DB label';
}
}

View file

@ -0,0 +1,59 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Services\LabelSystem;
use App\Entity\LabelSystem\LabelProfile;
use App\Repository\LabelProfileRepository;
use App\Services\UserCacheKeyGenerator;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\Cache\TagAwareCacheInterface;
final class LabelProfileDropdownHelper
{
private $cache;
private $entityManager;
private $keyGenerator;
public function __construct(TagAwareCacheInterface $treeCache, EntityManagerInterface $entityManager, UserCacheKeyGenerator $keyGenerator)
{
$this->cache = $treeCache;
$this->entityManager = $entityManager;
$this->keyGenerator = $keyGenerator;
}
public function getDropdownProfiles(string $type): array
{
$secure_class_name = str_replace('\\', '_', LabelProfile::class);
$key = 'profile_dropdown_'.$this->keyGenerator->generateKey().'_'.$secure_class_name . '_' . $type;
/** @var LabelProfileRepository $repo */
$repo = $this->entityManager->getRepository(LabelProfile::class);
return $this->cache->get($key, function (ItemInterface $item) use ($repo, $type, $secure_class_name) {
// Invalidate when groups, a element with the class or the user changes
$item->tag(['groups', 'tree_treeview', $this->keyGenerator->generateKey(), $secure_class_name]);
return $repo->getDropdownProfiles($type);
});
}
}

View file

@ -0,0 +1,75 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Services\LabelSystem;
use App\Services\LabelSystem\PlaceholderProviders\PlaceholderProviderInterface;
/**
* This service replaces the Placeholders of the user provided lines with the proper informations.
* It uses the PlaceholderProviders provided by PlaceholderProviderInterface classes.
* @package App\Services\LabelSystem
*/
final class LabelTextReplacer
{
private $providers;
public function __construct(iterable $providers)
{
$this->providers = $providers;
}
/**
* Determine the replacement for a single placeholder. It is iterated over all Replacement Providers.
* If the given string is not a placeholder or the placeholder is not known, it will be returned unchanged.
* @param string $placeholder The placeholder that should be replaced. (e.g. '%%PLACEHOLDER%%')
* @param object $target The object that should be used for the placeholder info source.
* @return string If the placeholder was valid, the replaced info. Otherwise the passed string.
*/
public function handlePlaceholder(string $placeholder, object $target): string
{
foreach ($this->providers as $provider) {
/** @var PlaceholderProviderInterface $provider */
$ret = $provider->replace($placeholder, $target);
if ($ret !== null) {
return $ret;
}
}
return $placeholder;
}
/**
* Replaces all placeholders in the input lines.
* @param string $lines The input lines that should be replaced
* @param object $target The object that should be used as source for the informations.
* @return string The Lines with replaced informations.
*/
public function replace(string $lines, object $target): string
{
$patterns = [
'/(\[\[[A-Z_]+\]\])/' => function ($match) use ($target) {
return $this->handlePlaceholder($match[0], $target);
},
];
return preg_replace_callback_array($patterns, $lines);
}
}

View file

@ -0,0 +1,55 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Services\LabelSystem\PlaceholderProviders;
use App\Entity\Base\AbstractDBElement;
use App\Services\ElementTypeNameGenerator;
final class AbstractDBElementProvider implements PlaceholderProviderInterface
{
private $elementTypeNameGenerator;
public function __construct(ElementTypeNameGenerator $elementTypeNameGenerator)
{
$this->elementTypeNameGenerator = $elementTypeNameGenerator;
}
/**
* @inheritDoc
*/
public function replace(string $placeholder, object $label_target, array $options = []): ?string
{
if ($label_target instanceof AbstractDBElement) {
if ($placeholder === '[[TYPE]]') {
return $this->elementTypeNameGenerator->getLocalizedTypeLabel($label_target);
}
if ($placeholder === '[[ID]]') {
return (string) ($label_target->getID() ?? 'unknown');
}
}
return null;
}
}

View file

@ -0,0 +1,107 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Services\LabelSystem\PlaceholderProviders;
use App\Entity\UserSystem\User;
use IntlDateFormatter;
use Locale;
use Symfony\Component\Security\Core\Security;
/**
* Provides Placeholders for infos about global infos like Installation name or datetimes.
* @package App\Services\LabelSystem\PlaceholderProviders
*/
final class GlobalProviders implements PlaceholderProviderInterface
{
private $partdb_title;
private $security;
public function __construct(string $partdb_title, Security $security)
{
$this->partdb_title = $partdb_title;
$this->security = $security;
}
/**
* @inheritDoc
*/
public function replace(string $placeholder, object $label_target, array $options = []): ?string
{
if ($placeholder === "[[INSTALL_NAME]]") {
return $this->partdb_title;
}
$user = $this->security->getUser();
if ($placeholder === "[[USERNAME]]") {
if ($user instanceof User) {
return $user->getName();
}
return 'anonymous';
}
if ($placeholder === "[[USERNAME_FULL]]") {
if ($user instanceof User) {
return $user->getFullName(true);
}
return 'anonymous';
}
$now = new \DateTime();
if ($placeholder === '[[DATETIME]]') {
$formatter = IntlDateFormatter::create(
Locale::getDefault(),
IntlDateFormatter::SHORT,
IntlDateFormatter::SHORT,
$now->getTimezone()
);
return $formatter->format($now);
}
if ($placeholder === '[[DATE]]') {
$formatter = IntlDateFormatter::create(
Locale::getDefault(),
IntlDateFormatter::SHORT,
IntlDateFormatter::NONE,
$now->getTimezone()
);
return $formatter->format($now);
}
if ($placeholder === '[[TIME]]') {
$formatter = IntlDateFormatter::create(
Locale::getDefault(),
IntlDateFormatter::NONE,
IntlDateFormatter::SHORT,
$now->getTimezone()
);
return $formatter->format($now);
}
return null;
}
}

View file

@ -0,0 +1,40 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Services\LabelSystem\PlaceholderProviders;
use App\Entity\Contracts\NamedElementInterface;
final class NamedElementProvider implements PlaceholderProviderInterface
{
/**
* @inheritDoc
*/
public function replace(string $placeholder, object $label_target, array $options = []): ?string
{
if ($label_target instanceof NamedElementInterface && $placeholder === '[[NAME]]') {
return $label_target->getName();
}
return null;
}
}

View file

@ -0,0 +1,91 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Services\LabelSystem\PlaceholderProviders;
use App\Entity\Parts\PartLot;
use App\Services\AmountFormatter;
use App\Services\LabelSystem\LabelTextReplacer;
use IntlDateFormatter;
use Locale;
final class PartLotProvider implements PlaceholderProviderInterface
{
private $labelTextReplacer;
private $amountFormatter;
public function __construct(LabelTextReplacer $labelTextReplacer, AmountFormatter $amountFormatter)
{
$this->labelTextReplacer = $labelTextReplacer;
$this->amountFormatter = $amountFormatter;
}
public function replace(string $placeholder, object $label_target, array $options = []): ?string
{
if ($label_target instanceof PartLot) {
if ($placeholder === '[[LOT_ID]]') {
return $label_target->getID() ?? 'unknown';
}
if ($placeholder === '[[LOT_NAME]]') {
return $label_target->getName();
}
if ($placeholder === '[[LOT_COMMENT]]') {
return $label_target->getComment();
}
if ($placeholder === '[[EXPIRATION_DATE]]') {
if ($label_target->getExpirationDate() === null) {
return '';
}
$formatter = IntlDateFormatter::create(
Locale::getDefault(),
IntlDateFormatter::SHORT,
IntlDateFormatter::NONE
//$label_target->getExpirationDate()->getTimezone()
);
return $formatter->format($label_target->getExpirationDate());
}
if ($placeholder === '[[AMOUNT]]') {
if ($label_target->isInstockUnknown()) {
return '?';
}
return $this->amountFormatter->format($label_target->getAmount(), $label_target->getPart()->getPartUnit());
}
if ($placeholder === '[[LOCATION]]') {
return $label_target->getStorageLocation() ? $label_target->getStorageLocation()->getName() : '';
}
if ($placeholder === '[[LOCATION_FULL]]') {
return $label_target->getStorageLocation() ? $label_target->getStorageLocation()->getFullPath() : '';
}
return $this->labelTextReplacer->handlePlaceholder($placeholder, $label_target->getPart());
}
return null;
}
}

View file

@ -0,0 +1,112 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Services\LabelSystem\PlaceholderProviders;
use App\Entity\Parts\Part;
use App\Services\SIFormatter;
use Symfony\Contracts\Translation\TranslatorInterface;
final class PartProvider implements PlaceholderProviderInterface
{
private $siFormatter;
private $translator;
public function __construct(SIFormatter $SIFormatter, TranslatorInterface $translator)
{
$this->siFormatter = $SIFormatter;
$this->translator = $translator;
}
/**
* @inheritDoc
*/
public function replace(string $placeholder, object $part, array $options = []): ?string
{
if (!$part instanceof Part) {
return null;
}
if ($placeholder === '[[CATEGORY]]') {
return $part->getCategory() ? $part->getCategory()->getName() : '';
}
if ($placeholder === '[[CATEGORY_FULL]]') {
return $part->getCategory() ? $part->getCategory()->getFullPath() : '';
}
if ($placeholder === '[[MANUFACTURER]]') {
return $part->getManufacturer() ? $part->getManufacturer()->getName() : '';
}
if ($placeholder === '[[MANUFACTURER_FULL]]') {
return $part->getManufacturer() ? $part->getManufacturer()->getFullPath() : '';
}
if ($placeholder === '[[FOOTPRINT]]') {
return $part->getFootprint() ? $part->getFootprint()->getName() : '';
}
if ($placeholder === '[[FOOTPRINT_FULL]]') {
return $part->getFootprint() ? $part->getFootprint()->getFullPath() : '';
}
if ($placeholder === '[[MASS]]') {
return $part->getMass() ? $this->siFormatter->format($part->getMass(), 'g', 1) : '';
}
if ($placeholder === '[[MPN]]') {
return $part->getManufacturerProductNumber();
}
if ($placeholder === '[[TAGS]]') {
return $part->getTags();
}
if ($placeholder === '[[M_STATUS]]') {
if ($part->getManufacturingStatus() === '') {
return '';
}
return $this->translator->trans('m_status.' . $part->getManufacturingStatus());
}
$parsedown = new \Parsedown();
if ($placeholder === '[[DESCRIPTION]]') {
return $parsedown->line($part->getDescription());
}
if ($placeholder === '[[DESCRIPTION_T]]') {
return strip_tags($parsedown->line($part->getDescription()));
}
if ($placeholder === '[[COMMENT]]') {
return $parsedown->line($part->getComment());
}
if ($placeholder === '[[COMMENT_T]]') {
return strip_tags($parsedown->line($part->getComment()));
}
return null;
}
}

View file

@ -0,0 +1,36 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Services\LabelSystem\PlaceholderProviders;
interface PlaceholderProviderInterface
{
/**
* Determines the real value of this placeholder.
* If the placeholder is not supported, null must be returned.
* @param string $placeholder The placeholder (e.g. "%%PLACEHOLDER%%") that should be replaced
* @param object $label_target The object that is targeted by the label
* @param array $options A list of options that can be used to specify the generated output further.
* @return string|null The real value of this placeholder, null if not supported.
*/
public function replace(string $placeholder, object $label_target, array $options = []): ?string;
}

View file

@ -0,0 +1,52 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Services\LabelSystem\PlaceholderProviders;
use App\Entity\Base\AbstractDBElement;
use App\Entity\Base\AbstractStructuralDBElement;
final class StructuralDBElementProvider implements PlaceholderProviderInterface
{
public function replace(string $placeholder, object $label_target, array $options = []): ?string
{
if ($label_target instanceof AbstractStructuralDBElement) {
if ($placeholder === '[[COMMENT]]') {
return $label_target->getComment();
}
if ($placeholder === '[[COMMENT_T]]') {
return strip_tags($label_target->getComment());
}
if ($placeholder === '[[FULL_PATH]]') {
return $label_target->getFullPath();
}
if ($placeholder === '[[PARENT]]') {
return $label_target->getParent() ? $label_target->getParent()->getName() : '';
}
if ($placeholder === '[[PARENT_FULL_PATH]]') {
return $label_target->getParent() ? $label_target->getParent()->getFullPath() : '';
}
}
return null;
}
}

View file

@ -0,0 +1,48 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Services\LabelSystem\PlaceholderProviders;
use App\Entity\Contracts\TimeStampableInterface;
use IntlDateFormatter;
use Locale;
final class TimestampableElementProvider implements PlaceholderProviderInterface
{
/**
* @inheritDoc
*/
public function replace(string $placeholder, object $label_target, array $options = []): ?string
{
if ($label_target instanceof TimeStampableInterface) {
if ($placeholder === '[[LAST_MODIFIED]]') {
return IntlDateFormatter::formatObject($label_target->getLastModified() ?? new \DateTime(), IntlDateFormatter::SHORT, Locale::getDefault());
}
if ($placeholder === '[[CREATION_DATE]]') {
return IntlDateFormatter::formatObject($label_target->getAddedDate() ?? new \DateTime(), IntlDateFormatter::SHORT, Locale::getDefault());
}
}
return null;
}
}

View file

@ -0,0 +1,140 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Services\LabelSystem;
use App\Entity\Attachments\Attachment;
use App\Entity\Attachments\AttachmentContainingDBElement;
use App\Entity\Base\AbstractCompany;
use App\Entity\Base\AbstractDBElement;
use App\Entity\Base\AbstractNamedDBElement;
use App\Entity\Base\AbstractStructuralDBElement;
use App\Entity\Contracts\NamedElementInterface;
use App\Entity\Contracts\TimeStampableInterface;
use App\Entity\LabelSystem\LabelOptions;
use App\Entity\Parameters\AbstractParameter;
use App\Entity\Parts\MeasurementUnit;
use App\Entity\Parts\Part;
use App\Entity\Parts\PartLot;
use App\Entity\Parts\Storelocation;
use App\Entity\Parts\Supplier;
use App\Entity\PriceInformations\Currency;
use App\Entity\PriceInformations\Orderdetail;
use App\Entity\PriceInformations\Pricedetail;
use App\Entity\UserSystem\User;
use App\Twig\AppExtension;
use App\Twig\Sandbox\InheritanceSecurityPolicy;
use Twig\Environment;
use Twig\Extension\SandboxExtension;
use Twig\Extra\Intl\IntlExtension;
use Twig\Sandbox\SecurityPolicy;
use Twig\Sandbox\SecurityPolicyInterface;
final class SandboxedTwigProvider
{
private const ALLOWED_TAGS = ['apply', 'autoescape', 'do', 'for', 'if', 'set', 'verbatim', 'with'];
private const ALLOWED_FILTERS = ['abs', 'batch', 'capitalize', 'column', 'country_name',
'currency_name', 'currency_symbol', 'date', 'date_modify', 'default', 'escape', 'filter', 'first', 'format',
'format_currency', 'format_date', 'format_datetime', 'format_number', 'format_time', 'join', 'keys',
'language_name', 'last', 'length', 'locale_name', 'lower', 'map', 'merge', 'nl2br', 'raw', 'number_format',
'reduce', 'replace', 'reverse', 'slice', 'sort', 'spaceless', 'split', 'striptags', 'timezone_name', 'title',
'trim', 'upper', 'url_encode',
//Part-DB specific filters:
'moneyFormat', 'siFormat', 'amountFormat'];
private const ALLOWED_FUNCTIONS = ['date', 'html_classes', 'max', 'min', 'random', 'range'];
private const ALLOWED_METHODS = [
NamedElementInterface::class => ['getName'],
AbstractDBElement::class => ['getID', '__toString'],
TimeStampableInterface::class => ['getLastModified', 'getAddedDate'],
AbstractStructuralDBElement::class => ['isChildOf', 'isRoot', 'getParent', 'getComment', 'getLevel',
'getFullPath', 'getPathArray', 'getChildren', 'isNotSelectable'],
AbstractCompany::class => ['getAddress', 'getPhoneNumber', 'getFaxNumber', 'getEmailAddress', 'getWebsite'],
AttachmentContainingDBElement::class => ['getAttachments', 'getMasterPictureAttachment'],
Attachment::class => ['isPicture', 'is3DModel', 'isExternal', 'isSecure', 'isBuiltIn', 'getExtension',
'getElement', 'getURL', 'getFilename', 'getAttachmentType', 'getShowInTable'],
AbstractParameter::class => ['getFormattedValue', 'getGroup', 'getSymbol', 'getValueMin', 'getValueMax',
'getValueTypical', 'getUnit', 'getValueText'],
MeasurementUnit::class => ['getUnit', 'isInteger', 'useSIPrefix'],
PartLot::class => ['isExpired', 'getDescription', 'getComment', 'getExpirationDate', 'getStorageLocation',
'getPart', 'isInstockUnknown', 'getAmount', 'getNeedsRefill'],
Storelocation::class => ['isFull', 'isOnlySinglePart', 'isLimitToExistingParts', 'getStorageType'],
Supplier::class => ['getShippingCosts', 'getDefaultCurrency'],
Part::class => ['isNeedsReview', 'getTags', 'getMass', 'getDescription', 'isFavorite', 'getCategory',
'getFootprint', 'getPartLots', 'getPartUnit', 'useFloatAmount', 'getMinAmount', 'getAmountSum',
'getManufacturerProductUrl', 'getCustomProductURL', 'getManufacturingStatus', 'getManufacturer',
'getManufacturerProductNumber', 'getOrderdetails', 'isObsolete'],
Currency::class => ['getIsoCode', 'getInverseExchangeRate', 'getExchangeRate'],
Orderdetail::class => ['getPart', 'getSupplier', 'getSupplierPartNr', 'getObsolete',
'getPricedetails', 'findPriceForQty', ],
Pricedetail::class => ['getOrderdetail', 'getPrice', 'getPricePerUnit', 'getPriceRelatedQuantity',
'getMinDiscountQuantity', 'getCurrency'],
//Only allow very little information about users...
User::class => ['isAnonymousUser', 'getUsername', 'getFullName', 'getFirstName', 'getLastName',
'getDepartment', 'getEmail']
];
private const ALLOWED_PROPERTIES = [];
private $appExtension;
public function __construct(AppExtension $appExtension)
{
$this->appExtension = $appExtension;
}
public function getTwig(LabelOptions $options): Environment
{
if ($options->getLinesMode() !== 'twig') {
throw new \InvalidArgumentException('The LabelOptions must explicitly allow twig via lines_mode = "twig"!');
}
$loader = new \Twig\Loader\ArrayLoader([
'lines' => $options->getLines(),
]);
$twig = new Environment($loader);
//Second argument activate sandbox globally.
$sandbox = new SandboxExtension($this->getSecurityPolicy(), true);
$twig->addExtension($sandbox);
//Add IntlExtension
$twig->addExtension(new IntlExtension());
//Add Part-DB specific extension
$twig->addExtension($this->appExtension);
return $twig;
}
protected function getSecurityPolicy(): SecurityPolicyInterface
{
return new InheritanceSecurityPolicy(
self::ALLOWED_TAGS,
self::ALLOWED_FILTERS,
self::ALLOWED_METHODS,
self::ALLOWED_PROPERTIES,
self::ALLOWED_FUNCTIONS
);
}
}

View file

@ -0,0 +1,94 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Services\Misc;
/**
* This Parser allows to parse number ranges like 1-3, 4, 5
*/
class RangeParser
{
/**
* Converts the given range string to an array of all numbers in the given range.
* @param string $range_str A range string like '1-3, 5, 6'
* @return int[] An array with all numbers from the range (e.g. [1, 2, 3, 5, 6]
*/
public function parse(string $range_str): array
{
//Normalize number separator (we allow , and ;):
$range_str = str_replace(';', ',', $range_str);
$numbers = explode(',', $range_str);
$ranges = [];
foreach ($numbers as $number) {
$number = trim($number);
//Extract min / max if token is a range
$matches = [];
if (preg_match('/^(-?\s*\d+)\s*-\s*(-?\s*\d+)$/', $number, $matches)) {
$ranges[] = $this->generateMinMaxRange($matches[1], $matches[2]);
} elseif (is_numeric($number)) {
$ranges[] = [(int) $number];
} elseif (empty($number)) { //Allow empty tokens
continue;
} else {
throw new \InvalidArgumentException('Invalid range encoutered: ' . $number);
}
}
//Flatten ranges array
return array_merge([], ...$ranges);
}
/**
* Checks if the given string is a valid range.
* @param string $range_str The string that should be checked
* @return bool True if the string is valid, false if not.
*/
public function isValidRange(string $range_str): bool
{
try {
$this->parse($range_str);
return true;
} catch (\InvalidArgumentException $exception) {
return false;
}
}
protected function generateMinMaxRange(string $min, string $max): array
{
$min = (int) $min;
$max = (int) $max;
//Ensure that $max > $min
if ($min > $max) {
$a = $max;
$max = $min;
$min = $a;
}
$tmp = [];
while ($min <= $max) {
$tmp[] = $min;
$min++;
};
return $tmp;
}
}

View file

@ -105,6 +105,7 @@ class SIFormatter
* @param float $value The value that should be converted
* @param string $unit The unit that should be appended after the prefix
* @param int $decimals the number of decimals (after decimal dot) that should be outputed
* @return string The formatted value
*/
public function format(float $value, string $unit = '', int $decimals = 2): string
{

View file

@ -45,6 +45,7 @@ namespace App\Services\Trees;
use App\Entity\Attachments\AttachmentType;
use App\Entity\Attachments\PartAttachment;
use App\Entity\Devices\Device;
use App\Entity\LabelSystem\LabelProfile;
use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint;
use App\Entity\Parts\Manufacturer;
@ -104,6 +105,7 @@ class ToolsTreeBuilder
$item->tag(['tree_tools', 'groups', $this->keyGenerator->generateKey()]);
$tree = [];
$tree[] = new TreeViewNode($this->translator->trans('tree.tools.tools'), null, $this->getToolsNode());
$tree[] = new TreeViewNode($this->translator->trans('tree.tools.edit'), null, $this->getEditNodes());
$tree[] = new TreeViewNode($this->translator->trans('tree.tools.show'), null, $this->getShowNodes());
$tree[] = new TreeViewNode($this->translator->trans('tree.tools.system'), null, $this->getSystemNodes());
@ -112,6 +114,27 @@ class ToolsTreeBuilder
});
}
protected function getToolsNode(): array
{
$nodes = [];
if ($this->security->isGranted('@labels.create_labels')) {
$nodes[] = new TreeViewNode(
$this->translator->trans('tree.tools.tools.label_dialog'),
$this->urlGenerator->generate('label_dialog')
);
}
if ($this->security->isGranted('@tools.label_scanner')) {
$nodes[] = new TreeViewNode(
$this->translator->trans('tree.tools.tools.label_scanner'),
$this->urlGenerator->generate('scan_dialog')
);
}
return $nodes;
}
/**
* This functions creates a tree entries for the "edit" node of the tool's tree.
*
@ -175,6 +198,12 @@ class ToolsTreeBuilder
$this->urlGenerator->generate('measurement_unit_new')
);
}
if ($this->security->isGranted('read', new LabelProfile())) {
$nodes[] = new TreeViewNode(
$this->translator->trans('tree.tools.edit.label_profile'),
$this->urlGenerator->generate('label_profile_new')
);
}
if ($this->security->isGranted('create', new Part())) {
$nodes[] = new TreeViewNode(
$this->translator->trans('tree.tools.edit.part'),
@ -182,6 +211,7 @@ class ToolsTreeBuilder
);
}
return $nodes;
}

View file

@ -118,10 +118,15 @@ class TreeViewGenerator
$item->addTag((string) \count($item->getNodes()));
}
if (! empty($href_type)) {
if (! empty($href_type) && $item->getId() !== null) {
$entity = $this->em->getPartialReference($class, $item->getId());
$item->setHref($this->urlGenerator->getURL($entity, $href_type));
}
//Translate text if text starts with $$
if (substr($item->getText(), 0, 2) === '$$') {
$item->setText($this->translator->trans(substr($item->getText(), 2)));
}
}
return array_merge($head, $generic);

View file

@ -0,0 +1,140 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Twig\Sandbox;
use Twig\Markup;
use Twig\Sandbox\SecurityNotAllowedFilterError;
use Twig\Sandbox\SecurityNotAllowedFunctionError;
use Twig\Sandbox\SecurityNotAllowedMethodError;
use Twig\Sandbox\SecurityNotAllowedPropertyError;
use Twig\Sandbox\SecurityNotAllowedTagError;
use Twig\Sandbox\SecurityPolicyInterface;
use Twig\Template;
/**
* Represents a security policy which need to be enforced when sandbox mode is enabled.
*
* Modified by Jan Böhmer, to allow inheritance of methods and properties via class hierachy.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class InheritanceSecurityPolicy implements SecurityPolicyInterface
{
private $allowedTags;
private $allowedFilters;
private $allowedMethods;
private $allowedProperties;
private $allowedFunctions;
public function __construct(array $allowedTags = [], array $allowedFilters = [], array $allowedMethods = [], array $allowedProperties = [], array $allowedFunctions = [])
{
$this->allowedTags = $allowedTags;
$this->allowedFilters = $allowedFilters;
$this->setAllowedMethods($allowedMethods);
$this->allowedProperties = $allowedProperties;
$this->allowedFunctions = $allowedFunctions;
}
public function setAllowedTags(array $tags): void
{
$this->allowedTags = $tags;
}
public function setAllowedFilters(array $filters): void
{
$this->allowedFilters = $filters;
}
public function setAllowedMethods(array $methods): void
{
$this->allowedMethods = [];
foreach ($methods as $class => $m) {
$this->allowedMethods[$class] = array_map(function ($value) { return strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); }, \is_array($m) ? $m : [$m]);
}
}
public function setAllowedProperties(array $properties): void
{
$this->allowedProperties = $properties;
}
public function setAllowedFunctions(array $functions): void
{
$this->allowedFunctions = $functions;
}
public function checkSecurity($tags, $filters, $functions): void
{
foreach ($tags as $tag) {
if (!\in_array($tag, $this->allowedTags)) {
throw new SecurityNotAllowedTagError(sprintf('Tag "%s" is not allowed.', $tag), $tag);
}
}
foreach ($filters as $filter) {
if (!\in_array($filter, $this->allowedFilters)) {
throw new SecurityNotAllowedFilterError(sprintf('Filter "%s" is not allowed.', $filter), $filter);
}
}
foreach ($functions as $function) {
if (!\in_array($function, $this->allowedFunctions)) {
throw new SecurityNotAllowedFunctionError(sprintf('Function "%s" is not allowed.', $function), $function);
}
}
}
public function checkMethodAllowed($obj, $method): void
{
if ($obj instanceof Template || $obj instanceof Markup) {
return;
}
$allowed = false;
$method = strtr($method, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
foreach ($this->allowedMethods as $class => $methods) {
if ($obj instanceof $class) {
$allowed = \in_array($method, $methods);
//CHANGED: Only break if we the method is allowed, otherwise try it on the other methods
if ($allowed) {
break;
}
}
}
if (!$allowed) {
$class = \get_class($obj);
throw new SecurityNotAllowedMethodError(sprintf('Calling "%s" method on a "%s" object is not allowed.', $method, $class), $class, $method);
}
}
public function checkPropertyAllowed($obj, $property): void
{
$allowed = false;
foreach ($this->allowedProperties as $class => $properties) {
if ($obj instanceof $class) {
$allowed = \in_array($property, \is_array($properties) ? $properties : [$properties]);
//CHANGED: Only break if we the method is allowed, otherwise try it on the other methods
if ($allowed) {
break;
}
}
}
if (!$allowed) {
$class = \get_class($obj);
throw new SecurityNotAllowedPropertyError(sprintf('Calling "%s" property on a "%s" object is not allowed.', $property, $class), $class, $property);
}
}
}

View file

@ -0,0 +1,33 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Validator\Constraints\Misc;
use Symfony\Component\Validator\Constraint;
/**
* @Annotation
* @package App\Validator\Constraints\Misc
*/
class ValidRange extends Constraint
{
public $message = 'validator.invalid_range';
}

View file

@ -0,0 +1,61 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Validator\Constraints\Misc;
use App\Services\Misc\RangeParser;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;
class ValidRangeValidator extends ConstraintValidator
{
protected $rangeParser;
public function __construct(RangeParser $rangeParser)
{
$this->rangeParser = $rangeParser;
}
public function validate($value, Constraint $constraint)
{
if (!$constraint instanceof ValidRange) {
throw new UnexpectedTypeException($constraint, ValidRange::class);
}
// custom constraints should ignore null and empty values to allow
// other constraints (NotBlank, NotNull, etc.) take care of that
if (null === $value || '' === $value) {
return;
}
if (!is_string($value)) {
throw new UnexpectedValueException($value, 'string');
}
if(!$this->rangeParser->isValidRange($value)) {
$this->context->buildViolation($constraint->message)
->addViolation();
}
}
}

View file

@ -120,12 +120,18 @@
"doctrine/reflection": {
"version": "v1.0.0"
},
"dompdf/dompdf": {
"version": "v0.8.5"
},
"egulias/email-validator": {
"version": "2.1.11"
},
"ekino/phpstan-banned-code": {
"version": "v0.3.1"
},
"erusev/parsedown": {
"version": "1.7.4"
},
"felixfbecker/advanced-json-rpc": {
"version": "v3.0.4"
},
@ -205,24 +211,12 @@
"netresearch/jsonmapper": {
"version": "v1.6.0"
},
"nette/application": {
"version": "v3.0.3"
},
"nette/component-model": {
"version": "v3.0.0"
},
"nette/finder": {
"version": "v2.5.2"
},
"nette/http": {
"version": "v3.0.3"
},
"nette/robot-loader": {
"version": "v3.2.1"
},
"nette/routing": {
"version": "v3.0.0"
},
"nette/utils": {
"version": "v3.1.0"
},
@ -271,6 +265,12 @@
"paragonie/random_compat": {
"version": "v9.99.99"
},
"phenx/php-font-lib": {
"version": "0.5.1"
},
"phenx/php-svg-lib": {
"version": "v0.3.3"
},
"php": {
"version": "7.2.5"
},
@ -373,6 +373,9 @@
"s9e/text-formatter": {
"version": "2.1.2"
},
"sabberworm/php-css-parser": {
"version": "8.3.0"
},
"scheb/two-factor-bundle": {
"version": "3.16",
"recipe": {
@ -656,6 +659,9 @@
"symfony/polyfill-mbstring": {
"version": "v1.10.0"
},
"symfony/polyfill-php70": {
"version": "v1.15.0"
},
"symfony/polyfill-php72": {
"version": "v1.10.0"
},
@ -830,18 +836,30 @@
"symplify/coding-standard": {
"version": "v7.1.3"
},
"symplify/console-color-diff": {
"version": "v7.3.10"
},
"symplify/easy-coding-standard": {
"version": "v7.1.3"
},
"symplify/package-builder": {
"version": "v7.1.3"
},
"symplify/phpstan-extensions": {
"version": "v7.3.10"
},
"symplify/set-config-resolver": {
"version": "v7.1.3"
},
"symplify/smart-file-system": {
"version": "v7.1.3"
},
"tecnickcom/tc-lib-barcode": {
"version": "1.15.20"
},
"tecnickcom/tc-lib-color": {
"version": "1.12.15"
},
"thecodingmachine/safe": {
"version": "v0.1.16"
},
@ -854,6 +872,9 @@
"twig/extra-bundle": {
"version": "v3.0.0"
},
"twig/html-extra": {
"version": "v3.0.3"
},
"twig/inky-extra": {
"version": "v3.0.0"
},

View file

@ -0,0 +1,52 @@
{% extends "AdminPages/EntityAdminBase.html.twig" %}
{% block card_title %}
<i class="fas fa-qrcode fa-fw"></i> {% trans %}label_profile.caption{% endtrans %}
{% endblock %}
{% block additional_pills %}
<li class="nav-item"><a data-toggle="tab" class="nav-link link-anchor" href="#tab_advanced">{% trans %}label_profile.advanced{% endtrans %}</a></li>
<li class="nav-item"><a data-toggle="tab" class="nav-link link-anchor" href="#tab_comment">{% trans %}label_profile.comment{% endtrans %}</a></li>
{% endblock %}
{% block additional_panes %}
<div class="tab-pane" id="tab_advanced">
{{ form_row(form.options.additional_css) }}
{{ form_widget(form.options) }}
</div>
<div class="tab-pane" id="tab_comment">
{{ form_widget(form.comment) }}
</div>
{% endblock %}
{% block comment %}{% endblock %}
{% block additional_controls %}
{{ form_row(form.show_in_dropdown) }}
{{ form_row(form.options.supported_element) }}
<div class="form-group row">
{{ form_label(form.options.width) }}
<div class="input-group col-9">
{{ form_widget(form.options.width) }}
<div class="input-group-append input-group-prepend">
<span class="input-group-text">x</span>
</div>
{{ form_widget(form.options.height) }}
<div class="input-group-append">
<span class="input-group-text">mm</span>
</div>
</div>
</div>
{{ form_row(form.options.barcode_type) }}
{{ form_row(form.options.lines) }}
{% endblock %}
{% block additional_content %}
{% if pdf_data is defined and pdf_data is not empty %}
<div class="card mt-2 mb-2 p-1 border-secondary" style="resize: vertical; overflow: scroll; height: 250px;">
<object id="pdf_preview" data="{{ pdf_data | data_uri(mime='application/pdf') }}"style="height: inherit">
</object>
</div>
{% endif %}
{% endblock %}

View file

@ -1,10 +1,18 @@
{% extends "AdminPages/EntityAdminBase.html.twig" %}
{% import "LabelSystem/dropdown_macro.html.twig" as dropdown %}
{% block card_title %}
<i class="fas fa-cube fa-fw"></i> {% trans %}storelocation.labelp{% endtrans %}
{% endblock %}
{% block additional_controls %}
{% if entity.id %}
<div class="row form-group">
<div class="offset-sm-3 col-sm-9">
{{ dropdown.profile_dropdown('storelocation', entity.id) }}
</div>
</div>
{% endif %}
{% endblock %}
{% block additional_pills %}

View file

@ -0,0 +1,33 @@
{% extends 'main_card.html.twig' %}
{% block card_title %}<i class="fas fa-atom fa-fw"></i> {% trans %}label_scanner.title{% endtrans %}{% endblock %}
{% block card_content %}
<div class="alert alert-warning" id="scanner-warning">
<strong>{% trans %}label_scanner.no_cam_found.title{% endtrans %}</strong>: {% trans %}label_scanner.no_cam_found.text{% endtrans %}
</div>
{{ form_start(form, {'attr': {'id': 'scan_dialog_form'}}) }}
{{ form_end(form) }}
<div class="">
<div class="form-group row">
<div class="offset-sm-3 col-sm-9">
<video id="video" width="100%" height="auto" class="scanner-video img-thumbnail d-none">
</video>
</div>
</div>
{# <div>
<button type="button" class="btn btn-secondary" id="changeSrcBtn">Change Source</button>
</div>#}
<div id="sourceSelectPanel" class="form-group row d-none">
<label for="sourceSelect" class="col-sm-3 col-form-label">{% trans %}label_scanner.source_select{% endtrans %}</label>
<div class="col-sm-9">
<select id="sourceSelect" style="" class="form-control"></select>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,116 @@
{% extends 'main_card.html.twig' %}
{% block title %}{% trans %}label_generator.title{% endtrans %}{% endblock %}
{%- block card_title -%}
<i class="fas fa-qrcode fa-fw"></i> {% trans %}label_generator.title{% endtrans %}
{% if profile %}({{ profile.name }}){% endif %}
{%- endblock -%}
{% block card_content %}
{{ form_start(form) }}
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link active" data-toggle="tab" id="common-tab" role="tab" aria-controls="common" aria-selected="true" href="#common"
>{% trans %}label_generator.common{% endtrans %}</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" id="advanced-tab" role="tab" aria-controls="advanced" aria-selected="false" href="#advanced"
>{% trans %}label_generator.advanced{% endtrans %}</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" id="profiles-tab" role="tab" aria-controls="profiles" aria-selected="false" href="#profiles"
>{% trans %}label_generator.profiles{% endtrans %}</a>
</li>
</ul>
<div class="tab-content mt-2">
<div class="tab-pane active" id="common" role="tabpanel" aria-labelledby="common-tab">
{{ form_row(form.target_id) }}
{{ form_row(form.options.supported_element) }}
<div class="form-group row">
{{ form_label(form.options.width) }}
<div class="input-group col-9">
{{ form_widget(form.options.width) }}
<div class="input-group-append input-group-prepend">
<span class="input-group-text">x</span>
</div>
{{ form_widget(form.options.height) }}
<div class="input-group-append">
<span class="input-group-text">mm</span>
</div>
</div>
</div>
{{ form_row(form.options.barcode_type) }}
{{ form_row(form.options.lines) }}
</div>
<div class="tab-pane" id="advanced" role="tabpanel" aria-labelledby="advanced-tab">
{{ form_row(form.options.additional_css) }}
{{ form_widget(form.options) }}
</div>
<div class="tab-pane" id="profiles" role="tabpanel" aria-labelledby="profiles-tab">
<div class="form-group row">
<label class="col-sm-3 col-form-label">{% trans %}label_generator.selected_profile{% endtrans %}</label>
<div class="col-sm-9">
<span class="form-control-plaintext">{{ profile.name ?? '-' }}
{% if profile %}
<a href="{{ profile | entityURL('edit') }}" title="{% trans %}label_generator.edit_profile{% endtrans %}"
><i class="fas fa-edit"></i></a>
{% endif %}
</span>
</div>
</div>
<div class="form-group row">
<div class="offset-sm-3 col-sm-9">
<div class="dropdown">
<button class="btn btn-info dropdown-toggle" type="button" id="loadProfilesButton"
{% if not is_granted("@labels.create_labels") %}disabled{% endif %}
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{% trans %}label_generator.load_profile{% endtrans %}
</button>
<div class="dropdown-menu" aria-labelledby="loadProfilesButton">
{% if is_granted("@labels.create_labels") %}
{% for type in constant("App\\Entity\\LabelSystem\\LabelOptions::SUPPORTED_ELEMENTS") %}
{% set profiles = label_profile_dropdown_helper.dropdownProfiles(type) %}
{% if profiles is not empty %}
<h6 class="dropdown-header">{{ (type~'.label') | trans }}</h6>
{% endif %}
{% for profile in profiles %}
<a class="dropdown-item" href="{{ path('label_dialog_profile', {'profile': profile.id }) }}">{{ profile.name }}</a>
{% endfor %}
{% endfor %}
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
{{ form_end(form) }}
{% if pdf_data %}
<div class="row">
<div class="col-sm-9 offset-sm-3">
<a data-no-ajax class="btn btn-secondary" href="#" onclick="this.href = document.getElementById('pdf_preview').data" download="{{ filename ?? '' }}">
{% trans %}label_generator.download{% endtrans %}
</a>
</div>
</div>
{% endif %}
{% endblock %}
{% block additional_content %}
{% if pdf_data %}
<div class="card mt-2 p-1 border-secondary" style="resize: vertical; overflow: scroll; height: 250px">
<object id="pdf_preview" data="{{ pdf_data | data_uri(mime='application/pdf') }}"style="height: inherit">
</object>
</div>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,24 @@
{% macro profile_dropdown(type, id = null, include_text = true, btn_type = 'btn-secondary') %}
<div class="dropdown">
<button type="button" class="btn {{ btn_type }} dropdown-toggle" title="{% trans %}label_generator.label_btn{% endtrans %}"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" {% if not is_granted("@labels.create_labels") %}disabled{% endif %}>
<i class="fas fa-fw fa-qrcode"></i> {% if include_text %}{% trans %}label_generator.label_btn{% endtrans %}{% endif %}
</button>
<div class="dropdown-menu">
{% if is_granted('@labels.read_profiles') %}
{% set profiles = label_profile_dropdown_helper.dropdownProfiles(type) %}
{% else %}
{% set profiles = [] %}
{% endif %}
{% for profile in profiles %}
<a class="dropdown-item" href="{{ path('label_dialog_profile', {'profile': profile.id, 'target_type': type, 'target_id': id, 'generate': true}) }}">{{ profile.name }}</a>
{% endfor %}
{% if profiles is not empty and is_granted('@labels.edit_options') %}
<div class="dropdown-divider"></div>
{% endif %}
{% if is_granted('@labels.edit_options') %} {# An empty dialog does not make much sense, when you can not edit the options... #}
<a class="dropdown-item" href="{{ path('label_dialog', {'target_type': type, 'target_id': id}) }}">{% trans %}label_generator.label_empty{% endtrans %}</a>
{% endif %}
</div>
</div>
{% endmacro %}

View file

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{{ meta_title }}</title>
<meta name="author" content="{{ partdb_title }}">
<meta name="description" content="Label for {{ meta_title }}">
<meta name="keywords" content="Part-DB, Label, Barcode">
<style>
{% include("LabelSystem/labels/label_style.css.twig") %}
{{ options.additionalCss | escape("html") }}
</style>
</head>
<body>
{% for element in elements %}
<div style="page-break-inside: avoid; page-break-before: avoid; {% if not loop.last %}page-break-after: always;{% endif %}">
{% if options.barcodeType == 'none' %}
{% include "LabelSystem/labels/label_page_none.html.twig" %}
{% elseif options.barcodeType in ['qr', 'datamatrix'] %}
{% include "LabelSystem/labels/label_page_qr.html.twig" %}
{% elseif options.barcodeType in ['code39', 'code93', 'code128'] %}
{% include "LabelSystem/labels/label_page_1d.html.twig" %}
{% endif %}
</div>
{% endfor %}
</body>
</html>

View file

@ -0,0 +1,27 @@
<div>
<div class="lines-container">
{{ element.lines | raw }}
</div>
<div class="C39-container" style="">
<img class="C39" src="{{ element.barcode | data_uri(mime='image/svg+xml') }}" height="30px">
<span class="C39-text">{{ element.barcode_content }}</span>
</div>
</div>
{#
<div>
<div class="row">
<div class="col-5">
<div class="C39-container">
<img class="C39" src="{{ inlineData(element.barcode, 'image/svg+xml') }}" width="100%" height="50px" style="">
</div>
</div>
<div class="col-7">
<div class="lines-container">
{{ element.lines | raw }}
</div>
</div>
</div>
</div>
#}

View file

@ -0,0 +1 @@
{{ element.lines | raw }}

View file

@ -0,0 +1,17 @@
<div>
<div class="row">
<div class="col-5">
<div class="qr-container">
<a href="{{ element.barcode_content }}">
<img class="qr" src="{{ element.barcode | data_uri(mime='image/svg+xml') }}" width="100%" style="">
</a>
</div>
{#{{ element.barcode | raw }} #}
</div>
<div class="col-7">
<div class="lines-container">
{{ element.lines | raw }}
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,157 @@
@page {
margin: 12px 6px;
}
body {
font-family: "DejaVu Sans Mono";
font-size: 9pt;
line-height: 1.0;
}
p {
margin: 0;
}
hr {
margin: 2px;
}
.qr {
max-width: 80%;
}
.qr-container a {
display: block;
}
.C39 {
max-width: 150px;
}
.C39-container {
display: inline-block;
align-content: center;
text-align: left;
position: fixed;
bottom: 32px;
}
.C39-text {
display: block;
margin-top: -6px;
font-size: 6pt;
}
/**************************************
Grid system token from simplegrid.io
SIMPLE GRID
(C) ZACH COLE 2016
************************************/
.font-light {
font-weight: 300;
}
.font-regular {
font-weight: 400;
}
.font-heavy {
font-weight: 700;
}
/* POSITIONING */
.left {
text-align: left;
}
.right {
text-align: right;
}
.center {
text-align: center;
margin-left: auto;
margin-right: auto;
}
.justify {
text-align: justify;
}
/* ==== GRID SYSTEM ==== */
.container {
width: 90%;
margin-left: auto;
margin-right: auto;
}
.row {
position: relative;
width: 100%;
page-break-after: avoid;
page-break-before: avoid;
page-break-inside: avoid;
}
.row [class^="col"] {
float: left;
margin: 0 0.5%;
min-height: 0.125rem ;
}
.row::after {
content: "";
display: table;
clear: both;
}
.col-1 {
width: 4.33%;
}
.col-2 {
width: 12.66%;
}
.col-3 {
width: 21%;
}
.col-4 {
width: 29.33%;
}
.col-5 {
width: 37.66%;
}
.col-6 {
width: 46%;
}
.col-7 {
width: 54.33%;
}
.col-8 {
width: 62.66%;
}
.col-9 {
width: 71%;
}
.col-10 {
width: 79.33%;
}
.col-11 {
width: 87.66%;
}
.col-12 {
width: 96%;
}

View file

@ -1,4 +1,5 @@
{% import "helper.twig" as helper %}
{% import "LabelSystem/dropdown_macro.html.twig" as dropdown %}
<table class="table table-striped table-hover table-responsive-sm">
<thead>
@ -7,6 +8,7 @@
<th>{% trans %}part_lots.storage_location{% endtrans %}</th>
<th>{% trans %}part_lots.amount{% endtrans %}</th>
<th></th> {# Tags row #}
<th></th> {# Button row #}
</tr>
</thead>
@ -55,6 +57,9 @@
{% endif %}
</h6>
</td>
<td>
{{ dropdown.profile_dropdown('part_lot', lot.id, false) }}
</td>
</tr>
{% endfor %}
</tbody>

View file

@ -1,3 +1,5 @@
{% import "LabelSystem/dropdown_macro.html.twig" as dropdown %}
{% if is_granted('edit', part) %}
<a href="{{ part|entityURL('edit') }}" class="btn btn-primary mt-3">
<i class="fas fa-fw fa-edit"></i>
@ -47,3 +49,5 @@
</div>
</div>
</form>
{{ dropdown.profile_dropdown('part', part.id) }}

View file

@ -1,4 +1,5 @@
{% import "helper.twig" as helper %}
{% import "LabelSystem/dropdown_macro.html.twig" as dropdown %}
{{ helper.breadcrumb_entity_link(entity) }}
@ -77,6 +78,11 @@
<i class="fas fa-calendar-plus fa-fw"></i> {{ entity.addedDate | format_datetime("short") }}
</span>
</div>
{% if entity is instanceof("App\\Entity\\Parts\\Storelocation") %}
{{ dropdown.profile_dropdown('storelocation', entity.id, true, 'btn-secondary btn-block mt-2') }}
{% endif %}
</div>
</div>
</div>

View file

@ -16,17 +16,19 @@
<div class="collapse navbar-collapse" id="navbarContent">
<ul class="navbar-nav mr-auto">
{% if is_granted("@tools.label_scanner") %}
<li class="nav-item">
<a class="nav-link" href="{{ url("scan_dialog") }}">{% trans %}navbar.scanner.link{% endtrans %}</a>
</li>
{% endif %}
</ul>
<div class="form-inline my-2 my-lg-0 ml-auto" id="searchbar">
<!-- Searchbar -->
{% include "_navbar_search.html.twig" %}
</div>
<a class="d-block d-md-none nav-link link-datasheet" style="color: black;"
href="zxing://scan/?ret={if isset($smarty.server.HTTPS)}https{else}http{/if}%3A%2F%2F{$smarty.server.HTTP_HOST|escape:'url'}{$relative_path|escape:'url'}show_search_parts.php%3Fkeyword%3D%7BCODE%7D&SCAN_FORMATS=EAN_8,CODE_39">
<i class="fa fa-barcode fa-fw fa-lg" aria-hidden="true"></i>
<span class="ml-2 text-muted">{% trans %}barcode.scan{% endtrans %}</span>
</a>
<ul class="navbar-nav ml-3" id="login-content">
<li class="nav-item dropdown">
<a href="#" class="dropdown-toggle link-anchor nav-link" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">

View file

@ -119,5 +119,12 @@ class ApplicationAvailabilityFunctionalTest extends WebTestCase
//Typeahead
yield ['/typeahead/builtInResources/search/DIP8'];
yield ['/typeahead/tags/search/test'];
//Label test
yield ['/scan'];
yield ['/label/dialog'];
yield ['/label/dialog?target_id=1&target_type=part'];
yield ['/label/1/dialog'];
yield ['/label/1/dialog?target_id=1&target_type=part&generate=1'];
}
}

View file

@ -0,0 +1,58 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Tests\Controller\AdminPages;
use App\Entity\LabelSystem\LabelProfile;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
class LabelProfileControllerTest extends AbstractAdminControllerTest
{
protected static $base_path = '/en'.'/label_profile';
protected static $entity_class = LabelProfile::class;
/**
* Tests if deleting an entity is working.
*
* @group slow
* @dataProvider deleteDataProvider
*/
public function testDeleteEntity(string $user, bool $delete): void
{
//Test read access
$client = static::createClient([], [
'PHP_AUTH_USER' => $user,
'PHP_AUTH_PW' => 'test',
]);
$client->catchExceptions(false);
if (false === $delete) {
$this->expectException(AccessDeniedException::class);
}
//Test read/list access by access /new overview page
$client->request('DELETE', static::$base_path.'/3');
//Page is redirected to '/new', when delete was successful
$this->assertSame($delete, $client->getResponse()->isRedirect(static::$base_path.'/new'));
$this->assertSame($delete, ! $client->getResponse()->isForbidden(), 'Permission Checking not working!');
}
}

View file

@ -0,0 +1,80 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Tests\Services\LabelSystem;
use App\Entity\LabelSystem\LabelOptions;
use App\Entity\Parts\Part;
use App\Services\LabelSystem\BarcodeGenerator;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
final class BarcodeGeneratorTest extends WebTestCase
{
/** @var BarcodeGenerator */
protected $services;
public function setUp(): void
{
self::bootKernel();
$this->services = self::$container->get(BarcodeGenerator::class);
}
public function testGetContent(): void
{
$part = new Part();
$part->setName('Test');
//Test that all barcodes types are supported
foreach (LabelOptions::BARCODE_TYPES as $type) {
$options = new LabelOptions();
$options->setBarcodeType($type);
$content = $this->services->generateSVG($options, $part);
//When type is none, service must return null.
if ($type === 'none') {
$this->assertNull($content);
} else {
$this->assertIsString($content);
}
}
}
public function testGenerateSVG(): void
{
$part = new Part();
$part->setName('Test');
//Test that all barcodes types are supported
foreach (LabelOptions::BARCODE_TYPES as $type) {
$options = new LabelOptions();
$options->setBarcodeType($type);
$svg = $this->services->generateSVG($options, $part);
//When type is none, service must return null.
if ($type === "none") {
$this->assertNull($svg);
} else {
$this->assertStringContainsStringIgnoringCase("SVG", $svg);
}
}
}
}

Some files were not shown because too many files have changed in this diff Show more