diff --git a/.github/workflows/docker_build.yaml b/.github/workflows/docker_build.yaml index 89d52664e4..26fc69a0f5 100644 --- a/.github/workflows/docker_build.yaml +++ b/.github/workflows/docker_build.yaml @@ -8,7 +8,7 @@ on: - 'master' jobs: - + docker: runs-on: ubuntu-latest diff --git a/.github/workflows/mysql.yaml b/.github/workflows/mysql.yaml index 087a866fbd..5bafe56253 100644 --- a/.github/workflows/mysql.yaml +++ b/.github/workflows/mysql.yaml @@ -33,7 +33,7 @@ jobs: options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 ports: - 3306:3306 - + steps: - name: Checkout Code uses: actions/checkout@v2 diff --git a/.github/workflows/postgresql.yaml b/.github/workflows/postgresql.yaml index ae8f52b962..3481895d85 100644 --- a/.github/workflows/postgresql.yaml +++ b/.github/workflows/postgresql.yaml @@ -5,7 +5,7 @@ name: PostgreSQL on: ["push", "pull_request"] jobs: - + test: runs-on: ubuntu-latest diff --git a/InvenTree/InvenTree/api_tester.py b/InvenTree/InvenTree/api_tester.py index 2e69e40969..eb92bd80c1 100644 --- a/InvenTree/InvenTree/api_tester.py +++ b/InvenTree/InvenTree/api_tester.py @@ -83,7 +83,7 @@ class InvenTreeAPITestCase(APITestCase): self.assertEqual(response.status_code, code) return response - + def post(self, url, data): """ Issue a POST request diff --git a/InvenTree/InvenTree/context.py b/InvenTree/InvenTree/context.py index e072f5a5ea..669b55b0c0 100644 --- a/InvenTree/InvenTree/context.py +++ b/InvenTree/InvenTree/context.py @@ -71,7 +71,7 @@ def status_codes(request): def user_roles(request): """ Return a map of the current roles assigned to the user. - + Roles are denoted by their simple names, and then the permission type. Permissions can be access as follows: diff --git a/InvenTree/InvenTree/exchange.py b/InvenTree/InvenTree/exchange.py index 04ceabccd8..06de4861ec 100644 --- a/InvenTree/InvenTree/exchange.py +++ b/InvenTree/InvenTree/exchange.py @@ -17,5 +17,5 @@ class InvenTreeManualExchangeBackend(BaseExchangeBackend): """ Do not get any rates... """ - + return {} diff --git a/InvenTree/InvenTree/fields.py b/InvenTree/InvenTree/fields.py index 155f77c639..c496c1bb22 100644 --- a/InvenTree/InvenTree/fields.py +++ b/InvenTree/InvenTree/fields.py @@ -102,5 +102,5 @@ class RoundingDecimalField(models.DecimalField): } defaults.update(kwargs) - + return super().formfield(**kwargs) diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index d5508b7db2..1097c5663b 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -35,7 +35,7 @@ def generateTestKey(test_name): """ Generate a test 'key' for a given test name. This must not have illegal chars as it will be used for dict lookup in a template. - + Tests must be named such that they will have unique keys. """ @@ -102,7 +102,7 @@ def TestIfImageURL(url): '.tif', '.tiff', '.webp', '.gif', ] - + def str2bool(text, test=True): """ Test if a string 'looks' like a boolean value. @@ -137,10 +137,10 @@ def isNull(text): """ Test if a string 'looks' like a null value. This is useful for querying the API against a null key. - + Args: text: Input text - + Returns: True if the text looks like a null value """ @@ -157,7 +157,7 @@ def normalize(d): d = Decimal(d) d = d.normalize() - + # Ref: https://docs.python.org/3/library/decimal.html return d.quantize(Decimal(1)) if d == d.to_integral() else d.normalize() @@ -165,14 +165,14 @@ def normalize(d): def increment(n): """ Attempt to increment an integer (or a string that looks like an integer!) - + e.g. 001 -> 002 2 -> 3 AB01 -> AB02 QQQ -> QQQ - + """ value = str(n).strip() @@ -314,7 +314,7 @@ def MakeBarcode(object_name, object_pk, object_data={}, **kwargs): def GetExportFormats(): """ Return a list of allowable file formats for exporting data """ - + return [ 'csv', 'tsv', @@ -327,7 +327,7 @@ def GetExportFormats(): def DownloadFile(data, filename, content_type='application/text'): """ Create a dynamic file for the user to download. - + Args: data: Raw file data (string or bytes) filename: Filename for the file download @@ -525,7 +525,7 @@ def addUserPermission(user, permission): """ Shortcut function for adding a certain permission to a user. """ - + perm = Permission.objects.get(codename=permission) user.user_permissions.add(perm) @@ -576,7 +576,7 @@ def getOldestMigrationFile(app, exclude_extension=True, ignore_initial=True): continue num = int(f.split('_')[0]) - + if oldest_file is None or num < oldest_num: oldest_num = num oldest_file = f @@ -585,7 +585,7 @@ def getOldestMigrationFile(app, exclude_extension=True, ignore_initial=True): oldest_file = oldest_file.replace('.py', '') return oldest_file - + def getNewestMigrationFile(app, exclude_extension=True): """ diff --git a/InvenTree/InvenTree/middleware.py b/InvenTree/InvenTree/middleware.py index f30d77ad3b..b905e86795 100644 --- a/InvenTree/InvenTree/middleware.py +++ b/InvenTree/InvenTree/middleware.py @@ -77,12 +77,20 @@ class AuthRequiredMiddleware(object): if request.path_info == reverse_lazy('logout'): return HttpResponseRedirect(reverse_lazy('login')) - login = reverse_lazy('login') + path = request.path_info - if not request.path_info == login and not request.path_info.startswith('/api/'): + # List of URL endpoints we *do not* want to redirect to + urls = [ + reverse_lazy('login'), + reverse_lazy('logout'), + reverse_lazy('admin:login'), + reverse_lazy('admin:logout'), + ] + + if path not in urls and not path.startswith('/api/'): # Save the 'next' parameter to pass through to the login view - return redirect('%s?next=%s' % (login, request.path)) + return redirect('%s?next=%s' % (reverse_lazy('login'), request.path)) # Code to be executed for each request/response after # the view is called. diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index 8494b52a10..5822f8a19f 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -129,7 +129,7 @@ class InvenTreeTree(MPTTModel): Here an 'item' is considered to be the 'leaf' at the end of each branch, and the exact nature here will depend on the class implementation. - + The default implementation returns zero """ return 0 diff --git a/InvenTree/InvenTree/permissions.py b/InvenTree/InvenTree/permissions.py index 973395a2a0..defb370435 100644 --- a/InvenTree/InvenTree/permissions.py +++ b/InvenTree/InvenTree/permissions.py @@ -17,7 +17,7 @@ class RolePermission(permissions.BasePermission): - PUT - PATCH - DELETE - + Specify the required "role" using the role_required attribute. e.g. diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index 6b40b8eb31..fa7674723c 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -44,7 +44,7 @@ class InvenTreeModelSerializer(serializers.ModelSerializer): In addition to running validators on the serializer fields, this class ensures that the underlying model is also validated. """ - + # Run any native validation checks first (may throw an ValidationError) data = super(serializers.ModelSerializer, self).validate(data) diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index e7b8aeb71e..e3191405d9 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -507,7 +507,7 @@ padding-right: 6px; padding-top: 3px; padding-bottom: 2px; -}; +} .panel-heading .badge { float: right; @@ -568,7 +568,7 @@ } .media { - //padding-top: 15px; + /* padding-top: 15px; */ overflow: visible; } @@ -594,8 +594,8 @@ width: 160px; position: fixed; z-index: 1; - //top: 0; - //left: 0; + /* top: 0; + left: 0; */ overflow-x: hidden; padding-top: 20px; padding-right: 25px; @@ -826,7 +826,7 @@ input[type="submit"] { width: 100%; padding: 20px; z-index: 5000; - pointer-events: none; // Prevent this div from blocking links underneath + pointer-events: none; /* Prevent this div from blocking links underneath */ } .alert { @@ -936,4 +936,15 @@ input[type="submit"] { input[type="date"].form-control, input[type="time"].form-control, input[type="datetime-local"].form-control, input[type="month"].form-control { line-height: unset; -} \ No newline at end of file +} + +.clip-btn { + font-size: 10px; + padding: 0px 6px; + color: var(--label-grey); + background: none; +} + +.clip-btn:hover { + background: var(--label-grey); +} diff --git a/InvenTree/InvenTree/static/script/clipboard.min.js b/InvenTree/InvenTree/static/script/clipboard.min.js new file mode 100644 index 0000000000..95f55d7b0c --- /dev/null +++ b/InvenTree/InvenTree/static/script/clipboard.min.js @@ -0,0 +1,7 @@ +/*! + * clipboard.js v2.0.8 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return n={134:function(t,e,n){"use strict";n.d(e,{default:function(){return r}});var e=n(279),i=n.n(e),e=n(370),a=n.n(e),e=n(817),o=n.n(e);function c(t){return(c="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function u(t,e){for(var n=0;n self.stock_item.quantity: errors['quantity'] = [_("Allocated quantity ({n}) must not exceed available quantity ({q})").format( diff --git a/InvenTree/build/templates/build/allocate.html b/InvenTree/build/templates/build/allocate.html index dee90a26a0..de07614c8e 100644 --- a/InvenTree/build/templates/build/allocate.html +++ b/InvenTree/build/templates/build/allocate.html @@ -86,7 +86,7 @@ } ); }); - + $("#btn-order-parts").click(function() { launchModalForm("/order/purchase-order/order-parts/", { data: { @@ -94,7 +94,7 @@ }, }); }); - + {% endif %} {% endblock %} diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html index c0f6e400b6..177fad8d6c 100644 --- a/InvenTree/build/templates/build/build_base.html +++ b/InvenTree/build/templates/build/build_base.html @@ -230,5 +230,5 @@ src="{% static 'img/blank_image.png' %}" } ); }); - + {% endblock %} \ No newline at end of file diff --git a/InvenTree/build/templates/build/index.html b/InvenTree/build/templates/build/index.html index 0ff4b78ab8..d5e6484d56 100644 --- a/InvenTree/build/templates/build/index.html +++ b/InvenTree/build/templates/build/index.html @@ -17,9 +17,9 @@
- +
- +
@@ -66,7 +66,7 @@ + + + @@ -173,7 +176,7 @@ $(document).ready(function () { {% endblock %} inventreeDocReady(); - + showCachedAlerts(); {% if barcodes %} diff --git a/InvenTree/templates/clip.html b/InvenTree/templates/clip.html new file mode 100644 index 0000000000..a56ece838c --- /dev/null +++ b/InvenTree/templates/clip.html @@ -0,0 +1,5 @@ +{% load i18n %} + + + + \ No newline at end of file diff --git a/InvenTree/templates/js/barcode.js b/InvenTree/templates/js/barcode.js index 990adc1a0b..8308086afb 100644 --- a/InvenTree/templates/js/barcode.js +++ b/InvenTree/templates/js/barcode.js @@ -75,7 +75,7 @@ function postBarcodeData(barcode_data, options={}) { enableBarcodeInput(modal, true); if (status == 'success') { - + if ('success' in response) { if (options.onScan) { options.onScan(response); @@ -125,7 +125,7 @@ function enableBarcodeInput(modal, enabled=true) { var barcode = $(modal + ' #barcode'); barcode.prop('disabled', !enabled); - + modalEnable(modal, enabled); barcode.focus(); @@ -157,14 +157,14 @@ function barcodeDialog(title, options={}) { var barcode = getBarcodeData(modal); if (barcode && barcode.length > 0) { - + postBarcodeData(barcode, options); } } $(modal).on('shown.bs.modal', function() { $(modal + ' .modal-form-content').scrollTop(0); - + var barcode = $(modal + ' #barcode'); // Handle 'enter' key on barcode @@ -210,10 +210,10 @@ function barcodeDialog(title, options={}) { var content = ''; content += `
{% trans "Scan barcode data below" %}
`; - + content += `
`; content += `
`; - + // Optional content before barcode input content += `
`; content += options.headerContent || ''; @@ -254,14 +254,14 @@ function barcodeScanDialog() { */ var modal = '#modal-form'; - + barcodeDialog( "Scan Barcode", { onScan: function(response) { if ('url' in response) { $(modal).modal('hide'); - + // Redirect to the URL! window.location.href = response.url; } else { @@ -399,7 +399,7 @@ function barcodeCheckIn(location_id, options={}) { break; } } - + if (match) { reloadTable(); } @@ -429,9 +429,9 @@ function barcodeCheckIn(location_id, options={}) { onSubmit: function() { // Called when the 'check-in' button is pressed - + var data = {location: location_id}; - + // Extract 'notes' field data.notes = $(modal + ' #notes').val(); diff --git a/InvenTree/templates/js/bom.js b/InvenTree/templates/js/bom.js index 5b62955499..462db6eba4 100644 --- a/InvenTree/templates/js/bom.js +++ b/InvenTree/templates/js/bom.js @@ -33,7 +33,7 @@ function removeRowFromBomWizard(e) { var colNum = 0; table.find('tr').each(function() { - + colNum++; if (colNum >= 3) { @@ -111,9 +111,9 @@ function loadBomTable(table, options) { if (options.part_detail) { params.part_detail = true; } - + params.sub_part_detail = true; - + var filters = {}; if (!options.disableFilters) { @@ -173,7 +173,7 @@ function loadBomTable(table, options) { // Display an extra icon if this part is an assembly if (sub_part.assembly) { var text = ``; - + html += renderLink(text, `/part/${row.sub_part}/bom/`); } @@ -182,7 +182,7 @@ function loadBomTable(table, options) { } ); - + // Part description cols.push( { @@ -325,7 +325,7 @@ function loadBomTable(table, options) { sortable: true, } ) - + // Part notes cols.push( { @@ -348,18 +348,18 @@ function loadBomTable(table, options) { if (row.part == options.parent_id) { var bValidate = ``; - + var bValid = ``; var bEdit = ``; var bDelt = ``; - + var html = "
"; - + html += bEdit; html += bDelt; - + if (!row.validated) { html += bValidate; } else { @@ -394,7 +394,7 @@ function loadBomTable(table, options) { { success: function(response) { for (var idx = 0; idx < response.length; idx++) { - + response[idx].parentId = bom_pk; if (response[idx].sub_part_detail.assembly) { @@ -412,7 +412,7 @@ function loadBomTable(table, options) { } ) } - + table.inventreeTable({ treeEnable: !options.editable, rootParentId: parent_id, @@ -497,7 +497,7 @@ function loadBomTable(table, options) { var pk = $(this).attr('pk'); var url = `/part/bom/${pk}/delete/`; - + launchModalForm( url, { @@ -509,7 +509,7 @@ function loadBomTable(table, options) { }); table.on('click', '.bom-edit-button', function() { - + var pk = $(this).attr('pk'); var url = `/part/bom/${pk}/edit/`; @@ -524,7 +524,7 @@ function loadBomTable(table, options) { }); table.on('click', '.bom-validate-button', function() { - + var pk = $(this).attr('pk'); var url = `/api/bom/${pk}/validate/`; diff --git a/InvenTree/templates/js/build.js b/InvenTree/templates/js/build.js index d4ceb9c909..0233974741 100644 --- a/InvenTree/templates/js/build.js +++ b/InvenTree/templates/js/build.js @@ -49,7 +49,7 @@ function makeBuildOutputActionButtons(output, buildInfo, lines) { function reloadTable() { $(panel).find(`#allocation-table-${outputId}`).bootstrapTable('refresh'); } - + // Find the div where the buttons will be displayed var buildActions = $(panel).find(`#output-actions-${outputId}`); @@ -82,7 +82,7 @@ function makeBuildOutputActionButtons(output, buildInfo, lines) { //disabled: true } ); - + // Add a button to "delete" the particular build output html += makeIconButton( 'fa-trash-alt icon-red', 'button-output-delete', outputId, @@ -171,7 +171,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { var partId = buildInfo.part; var outputId = null; - + if (output) { outputId = output.pk; } else { @@ -179,7 +179,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { } var table = options.table; - + if (options.table == null) { table = `#allocation-table-${outputId}`; } @@ -187,7 +187,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { // If an "output" is specified, then only "trackable" parts are allocated // Otherwise, only "untrackable" parts are allowed var trackable = ! !output; - + function reloadTable() { // Reload the entire build allocation table $(table).bootstrapTable('refresh'); @@ -492,7 +492,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { if (row.stock_item_detail.location) { var text = row.stock_item_detail.location_name; var url = `/stock/location/${row.stock_item_detail.location}/`; - + return renderLink(text, url); } else { return '{% trans "No location set" %}'; @@ -600,7 +600,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { qA *= output.quantity; qB *= output.quantity; - + // Handle the case where both numerators are zero if ((aA == 0) && (aB == 0)) { return (qA > qB) ? 1 : -1; @@ -610,7 +610,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { if ((qA == 0) || (qB == 0)) { return 1; } - + var progressA = parseFloat(aA) / qA; var progressB = parseFloat(aB) / qB; @@ -618,7 +618,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { if (progressA == progressB) { return (qA < qB) ? 1 : -1; } - + return (progressA < progressB) ? 1 : -1; } }, @@ -670,7 +670,7 @@ function loadBuildTable(table, options) { var filters = {}; params['part_detail'] = true; - + if (!options.disableFilters) { filters = loadTableFilters("build"); } @@ -801,11 +801,11 @@ function loadBuildTable(table, options) { function updateAllocationTotal(id, count, required) { - + count = parseFloat(count); - + $('#allocation-total-'+id).html(count); - + var el = $("#allocation-panel-" + id); el.removeClass('part-allocation-pass part-allocation-underallocated part-allocation-overallocated'); @@ -819,7 +819,7 @@ function updateAllocationTotal(id, count, required) { } function loadAllocationTable(table, part_id, part, url, required, button) { - + // Load the allocation table table.bootstrapTable({ url: url, @@ -848,9 +848,9 @@ function loadAllocationTable(table, part_id, part, url, required, button) { var bEdit = ""; var bDel = ""; - + html += "
" + bEdit + bDel + "
"; - + return html; } } @@ -992,7 +992,7 @@ function loadBuildPartsTable(table, options={}) { // Display an extra icon if this part is an assembly if (sub_part.assembly) { var text = ``; - + html += renderLink(text, `/part/${row.sub_part}/bom/`); } diff --git a/InvenTree/templates/js/company.js b/InvenTree/templates/js/company.js index d258c8bab1..be4b707466 100644 --- a/InvenTree/templates/js/company.js +++ b/InvenTree/templates/js/company.js @@ -39,11 +39,11 @@ function loadCompanyTable(table, url, options={}) { if (row.is_customer) { html += ``; } - + if (row.is_manufacturer) { html += ``; } - + if (row.is_supplier) { html += ``; } diff --git a/InvenTree/templates/js/filters.js b/InvenTree/templates/js/filters.js index 612af8e03c..a27e91d5dc 100644 --- a/InvenTree/templates/js/filters.js +++ b/InvenTree/templates/js/filters.js @@ -185,11 +185,11 @@ function getFilterOptionList(tableKey, filterKey) { function generateAvailableFilterList(tableKey) { var remaining = getRemainingTableFilters(tableKey); - + var id = 'filter-tag-' + tableKey.toLowerCase(); var html = `