2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-18 13:05:42 +00:00

Merge pull request #1716 from SchrodingersGat/drf-api-forms

[WIP] API Forms
This commit is contained in:
Oliver
2021-07-03 22:14:26 +10:00
committed by GitHub
225 changed files with 5557 additions and 3121 deletions

View File

@ -10,35 +10,6 @@
<div class='dropzone' id='attachment-dropzone'>
<table class='table table-striped table-condensed' data-toolbar='#attachment-buttons' id='attachment-table'>
<thead>
<tr>
<th data-field='file' data-sortable='true' data-searchable='true'>{% trans "File" %}</th>
<th data-field='comment' data-sortable='true' data-searchable='true'>{% trans "Comment" %}</th>
<th data-field='user' data-sortable='true' data-searchable='true'>{% trans "Uploaded" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for attachment in attachments %}
<tr>
<td><a href='/media/{{ attachment.attachment }}'>{{ attachment.basename }}</a></td>
<td>{{ attachment.comment }}</td>
<td>
{% if attachment.upload_date %}{{ attachment.upload_date }}{% endif %}
{% if attachment.user %}<span class='badge'>{{ attachment.user.username }}</div>{% endif %}
</td>
<td>
<div class='btn-group' style='float: right;'>
<button type='button' class='btn btn-default btn-glyph attachment-edit-button' pk="{{ attachment.id }}" data-toggle='tooltip' title='{% trans "Edit attachment" %}'>
<span class='fas fa-edit icon-blue'/>
</button>
<button type='button' class='btn btn-default btn-glyph attachment-delete-button' pk="{{ attachment.id }}" data-toggle='tooltip' title='{% trans "Delete attachment" %}'>
<span class='fas fa-trash-alt icon-red'/>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>

View File

@ -40,8 +40,8 @@
<link rel="stylesheet" href="{% static 'bootstrap-table/bootstrap-table.css' %}">
<link rel="stylesheet" href="{% static 'bootstrap-table/extensions/group-by-v2/bootstrap-table-group-by.css' %}">
<link rel="stylesheet" href="{% static 'bootstrap-table/extensions/filter-control/bootstrap-table-filter-control.css' %}">
<link rel="stylesheet" href="{% static 'css/select2.css' %}">
<link rel="stylesheet" href="{% static 'css/select2-bootstrap.css' %}">
<link rel="stylesheet" href="{% static 'select2/css/select2.css' %}">
<link rel="stylesheet" href="{% static 'select2/css/select2-bootstrap.css' %}">
<link rel="stylesheet" href="{% static 'css/bootstrap-toggle.css' %}">
<link rel="stylesheet" href="{% static 'fullcalendar/main.css' %}">
<link rel="stylesheet" href="{% static 'script/jquery-ui/jquery-ui.min.css' %}">
@ -136,7 +136,7 @@
<!-- 3rd party general js -->
<script type="text/javascript" src="{% static 'fullcalendar/main.js' %}"></script>
<script type="text/javascript" src="{% static 'fullcalendar/locales-all.js' %}"></script>
<script type="text/javascript" src="{% static 'script/select2/select2.js' %}"></script>
<script type="text/javascript" src="{% static 'select2/js/select2.full.js' %}"></script>
<script type='text/javascript' src="{% static 'script/moment.js' %}"></script>
<script type='text/javascript' src="{% static 'script/chart.min.js' %}"></script>
<script type='text/javascript' src="{% static 'script/clipboard.min.js' %}"></script>
@ -144,11 +144,14 @@
<!-- general InvenTree -->
<script type='text/javascript' src="{% static 'script/inventree/inventree.js' %}"></script>
<script type='text/javascript' src="{% static 'script/inventree/api.js' %}"></script>
<script type='text/javascript' src="{% static 'script/inventree/notification.js' %}"></script>
<script type='text/javascript' src="{% static 'script/inventree/sidenav.js' %}"></script>
<!-- translated -->
<script type='text/javascript' src="{% i18n_static 'api.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'attachment.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'forms.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'model_renderers.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'barcode.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'bom.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'company.js' %}"></script>

View File

@ -0,0 +1,141 @@
var jQuery = window.$;
// using jQuery
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie != '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = jQuery.trim(cookies[i]);
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) == (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
function inventreeGet(url, filters={}, options={}) {
// Middleware token required for data update
//var csrftoken = jQuery("[name=csrfmiddlewaretoken]").val();
var csrftoken = getCookie('csrftoken');
return $.ajax({
beforeSend: function(xhr, settings) {
xhr.setRequestHeader('X-CSRFToken', csrftoken);
},
url: url,
type: 'GET',
data: filters,
dataType: 'json',
contentType: 'application/json',
success: function(response) {
if (options.success) {
options.success(response);
}
},
error: function(xhr, ajaxOptions, thrownError) {
console.error('Error on GET at ' + url);
console.error(thrownError);
if (options.error) {
options.error({
error: thrownError
});
}
}
});
}
function inventreeFormDataUpload(url, data, options={}) {
/* Upload via AJAX using the FormData approach.
*
* Note that the following AJAX parameters are required for FormData upload
*
* processData: false
* contentType: false
*/
// CSRF cookie token
var csrftoken = getCookie('csrftoken');
return $.ajax({
beforeSend: function(xhr, settings) {
xhr.setRequestHeader('X-CSRFToken', csrftoken);
},
url: url,
method: options.method || 'POST',
data: data,
processData: false,
contentType: false,
success: function(data, status, xhr) {
if (options.success) {
options.success(data, status, xhr);
}
},
error: function(xhr, status, error) {
console.log('Form data upload failure: ' + status);
if (options.error) {
options.error(xhr, status, error);
}
}
});
}
function inventreePut(url, data={}, options={}) {
var method = options.method || 'PUT';
// Middleware token required for data update
//var csrftoken = jQuery("[name=csrfmiddlewaretoken]").val();
var csrftoken = getCookie('csrftoken');
return $.ajax({
beforeSend: function(xhr, settings) {
xhr.setRequestHeader('X-CSRFToken', csrftoken);
},
url: url,
type: method,
data: JSON.stringify(data),
dataType: 'json',
contentType: 'application/json',
success: function(response, status) {
if (options.success) {
options.success(response, status);
}
if (options.reloadOnSuccess) {
location.reload();
}
},
error: function(xhr, ajaxOptions, thrownError) {
if (options.error) {
options.error(xhr, ajaxOptions, thrownError);
} else {
console.error(`Error on ${method} to '${url}' - STATUS ${xhr.status}`);
console.error(thrownError);
}
},
complete: function(xhr, status) {
if (options.complete) {
options.complete(xhr, status);
}
}
});
}
function inventreeDelete(url, options={}) {
/*
* Delete a record
*/
options = options || {};
options.method = 'DELETE';
inventreePut(url, {}, options);
}

View File

@ -0,0 +1,86 @@
{% load i18n %}
function reloadAttachmentTable() {
$('#attachment-table').bootstrapTable("refresh");
}
function loadAttachmentTable(url, options) {
var table = options.table || '#attachment-table';
$(table).inventreeTable({
url: url,
name: options.name || 'attachments',
formatNoMatches: function() { return '{% trans "No attachments found" %}'},
sortable: true,
search: false,
queryParams: options.filters || {},
onPostBody: function() {
// Add callback for 'edit' button
$(table).find('.button-attachment-edit').click(function() {
var pk = $(this).attr('pk');
if (options.onEdit) {
options.onEdit(pk);
}
});
// Add callback for 'delete' button
$(table).find('.button-attachment-delete').click(function() {
var pk = $(this).attr('pk');
if (options.onDelete) {
options.onDelete(pk);
}
});
},
columns: [
{
field: 'attachment',
title: '{% trans "File" %}',
formatter: function(value, row) {
var split = value.split('/');
return renderLink(split[split.length - 1], value);
}
},
{
field: 'comment',
title: '{% trans "Comment" %}',
},
{
field: 'upload_date',
title: '{% trans "Upload Date" %}',
},
{
field: 'actions',
formatter: function(value, row) {
var html = '';
html = `<div class='btn-group float-right' role='group'>`;
html += makeIconButton(
'fa-edit icon-blue',
'button-attachment-edit',
row.pk,
'{% trans "Edit attachment" %}',
);
html += makeIconButton(
'fa-trash-alt icon-red',
'button-attachment-delete',
row.pk,
'{% trans "Delete attachment" %}',
);
html += `</div>`;
return html;
}
}
]
});
}

View File

@ -514,14 +514,13 @@ function loadBomTable(table, options) {
var pk = $(this).attr('pk');
var url = `/part/bom/${pk}/delete/`;
launchModalForm(
url,
{
success: function() {
reloadBomTable(table);
}
constructForm(`/api/bom/${pk}/`, {
method: 'DELETE',
title: '{% trans "Delete BOM Item" %}',
onSuccess: function() {
reloadBomTable(table);
}
);
});
});
table.on('click', '.bom-edit-button', function() {

View File

@ -1,5 +1,74 @@
{% load i18n %}
// Returns a default form-set for creating / editing a Company object
function companyFormFields(options={}) {
return {
name: {},
description: {},
website: {
icon: 'fa-globe',
},
address: {
icon: 'fa-envelope',
},
currency: {
icon: 'fa-dollar-sign',
},
phone: {
icon: 'fa-phone',
},
email: {
icon: 'fa-at',
},
contact: {
icon: 'fa-address-card',
},
is_supplier: {},
is_manufacturer: {},
is_customer: {}
};
}
function editCompany(pk, options={}) {
var fields = options.fields || companyFormFields();
constructForm(
`/api/company/${pk}/`,
{
method: 'PATCH',
fields: fields,
reload: true,
title: '{% trans "Edit Company" %}',
}
);
};
/*
* Launches a form to create a new company.
* As this can be called from many different contexts,
* we abstract it here!
*/
function createCompany(options={}) {
// Default field set
var fields = options.fields || companyFormFields();
constructForm(
'{% url "api-company-list" %}',
{
method: 'POST',
fields: fields,
follow: true,
title: '{% trans "Add new Company" %}',
}
);
}
function loadCompanyTable(table, url, options={}) {
/*
* Load company listing data into specified table.
@ -101,6 +170,61 @@ function loadCompanyTable(table, url, options={}) {
}
function deleteManufacturerParts(selections, options={}) {
if (selections.length == 0) {
return;
}
var parts = [];
var text = `
<div class='alert alert-block alert-danger'>
<p>{% trans "The following manufacturer parts will be deleted" %}:</p>
<ul>`;
selections.forEach(function(item) {
parts.push(item.pk);
text += `
<li>
<p>${item.MPN} - ${item.part_detail.full_name}</p>
</li>`;
});
text += `
</ul>
</div>`;
showQuestionDialog(
'{% trans "Delete Manufacturer Parts" %}',
text,
{
accept_text: '{% trans "Delete" %}',
accept: function() {
// Delete each manufacturer part
var requests = [];
parts.forEach(function(pk) {
var url = `/api/company/part/manufacturer/${pk}`;
requests.push(inventreeDelete(url));
});
// Wait for all the requests to complete
$.when.apply($, requests).then(function() {
if (options.onSuccess) {
options.onSuccess();
}
})
}
}
);
}
function loadManufacturerPartTable(table, url, options) {
/*
* Load manufacturer part table
@ -228,7 +352,7 @@ function loadManufacturerPartParameterTable(table, url, options) {
{
checkbox: true,
switchable: false,
visible: false,
visible: true,
},
{
field: 'name',
@ -273,27 +397,28 @@ function loadManufacturerPartParameterTable(table, url, options) {
$(table).find('.button-parameter-edit').click(function() {
var pk = $(this).attr('pk');
launchModalForm(
`/manufacturer-part/parameter/${pk}/edit/`,
{
success: function() {
$(table).bootstrapTable('refresh');
}
constructForm(`/api/company/part/manufacturer/parameter/${pk}/`, {
fields: {
name: {},
value: {},
units: {},
},
title: '{% trans "Edit Parameter" %}',
onSuccess: function() {
$(table).bootstrapTable('refresh');
}
);
});
});
$(table).find('.button-parameter-delete').click(function() {
var pk = $(this).attr('pk');
launchModalForm(
`/manufacturer-part/parameter/${pk}/delete/`,
{
success: function() {
$(table).bootstrapTable('refresh');
}
constructForm(`/api/company/part/manufacturer/parameter/${pk}/`, {
method: 'DELETE',
title: '{% trans "Delete Parameter" %}',
onSuccess: function() {
$(table).bootstrapTable('refresh');
}
);
});
});
}
});

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,114 @@
{% load i18n %}
/*
* Create and display a new modal dialog
*
* options:
* - title: Form title to render
* - submitText: Text to render on 'submit' button (default = "Submit")
* - closeText: Text to render on 'close' button (default = "Cancel")
* - focus: Name of field to focus on after launching
*/
function createNewModal(options={}) {
var id = 1;
// Check out what modal forms are already being displayed
$('.inventree-modal').each(function() {
var split = this.id.split('-');
var modal_id = parseInt(split[2]);
if (modal_id >= id) {
id = modal_id + 1;
}
});
var html = `
<div class='modal fade modal-fixed-footer modal-primary inventree-modal' role='dialog' id='modal-form-${id}'>
<div class='modal-dialog'>
<div class='modal-content'>
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label='{% trans "Close" %}'>
<span aria-hidden="true">&times;</span>
</button>
<h3 id='modal-title'>
<!-- Form title to be injected here -->
</h3>
</div>
<div class='modal-form-content-wrapper'>
<div id='pre-form-content'>
<!-- Content can be inserted here *before* the form fields -->
</div>
<div id='non-field-errors'>
<!-- Form error messages go here -->
</div>
<div id='form-content' class='modal-form-content'>
<!-- Form content will be injected here-->
</div>
<div id='post-form-content'>
<!-- Content can be inserted here *after* the form fields -->
</div>
</div>
<div class='modal-footer'>
<div id='modal-footer-buttons'>
<!-- Extra buttons can be inserted here -->
</div>
<button type='button' class='btn btn-default' id='modal-form-close' data-dismiss='modal'>{% trans "Cancel" %}</button>
<button type='button' class='btn btn-primary' id='modal-form-submit'>{% trans "Submit" %}</button>
</div>
</div>
</div>
</div>
`;
$('body').append(html);
var modal_name = `#modal-form-${id}`;
$(modal_name).on('shown.bs.modal', function() {
$(modal_name + ' .modal-form-content').scrollTop(0);
if (options.focus) {
getFieldByName(modal_name, options.focus).focus();
}
});
// Automatically remove the modal when it is deleted!
$(modal_name).on('hidden.bs.modal', function(e) {
$(modal_name).remove();
});
// Capture "enter" key input
$(modal_name).on('keydown', 'input', function(event) {
if (event.keyCode == 13) {
event.preventDefault();
// Simulate a click on the 'Submit' button
$(modal_name).find("#modal-form-submit").click();
return false;
}
});
$(modal_name).modal({
backdrop: 'static',
keyboard: false,
});
// Set labels based on supplied options
modalSetTitle(modal_name, options.title || '{% trans "Form Title" %}');
modalSetSubmitText(modal_name, options.submitText || '{% trans "Submit" %}');
modalSetCloseText(modal_name, options.cancelText || '{% trans "Cancel" %}');
// Return the "name" of the modal
return modal_name;
}
function makeOption(text, value, title) {
/* Format an option for a select element
*/
@ -991,8 +1100,6 @@ function hideModalImage() {
function showModalImage(image_url) {
// Display full-screen modal image
console.log('showing modal image: ' + image_url);
var modal = $('#modal-image-dialog');
// Set image content

View File

@ -0,0 +1,155 @@
{% load i18n %}
/*
* This file contains functions for rendering various InvenTree database models,
* in particular for displaying them in modal forms in a 'select2' context.
*
* Each renderer is provided with three arguments:
*
* - name: The 'name' of the model instance in the referring model
* - data: JSON data which represents the model instance. Returned via a GET request.
* - parameters: The field parameters provided via an OPTIONS request to the endpoint.
* - options: User options provided by the client
*/
// Renderer for "Company" model
function renderCompany(name, data, parameters, options) {
var html = `<span>${data.name}</span> - <i>${data.description}</i>`;
html += `<span class='float-right'>{% trans "Company ID" %}: ${data.pk}</span>`;
return html;
}
// Renderer for "StockItem" model
function renderStockItem(name, data, parameters, options) {
var image = data.part_detail.thumbnail || data.part_detail.image;
if (!image) {
image = `/static/img/blank_image.png`;
}
var html = `<img src='${image}' class='select2-thumbnail'>`;
html += ` <span>${data.part_detail.full_name || data.part_detail.name}</span>`;
if (data.serial && data.quantity == 1) {
html += ` - <i>{% trans "Serial Number" %}: ${data.serial}`;
} else {
html += ` - <i>{% trans "Quantity" %}: ${data.quantity}`;
}
if (data.part_detail.description) {
html += `<p><small>${data.part_detail.description}</small></p>`;
}
return html;
}
// Renderer for "StockLocation" model
function renderStockLocation(name, data, parameters, options) {
var html = `<span>${data.name}</span>`;
if (data.description) {
html += ` - <i>${data.description}</i>`;
}
html += `<span class='float-right'>{% trans "Location ID" %}: ${data.pk}</span>`;
if (data.pathstring) {
html += `<p><small>${data.pathstring}</small></p>`;
}
return html;
}
// Renderer for "Part" model
function renderPart(name, data, parameters, options) {
var image = data.image;
if (!image) {
image = `/static/img/blank_image.png`;
}
var html = `<img src='${image}' class='select2-thumbnail'>`;
html += ` <span>${data.full_name || data.name}</span>`;
if (data.description) {
html += ` - <i>${data.description}</i>`;
}
html += `<span class='float-right'>{% trans "Part ID" %}: ${data.pk}</span>`;
return html;
}
// Renderer for "Owner" model
function renderOwner(name, data, parameters, options) {
var html = `<span>${data.name}</span>`;
switch (data.label) {
case 'user':
html += `<span class='float-right fas fa-user'></span>`;
break;
case 'group':
html += `<span class='float-right fas fa-users'></span>`;
break;
default:
break;
}
return html;
}
// Renderer for "PartCategory" model
function renderPartCategory(name, data, parameters, options) {
var html = `<span><b>${data.name}</b></span>`;
if (data.description) {
html += ` - <i>${data.description}</i>`;
}
html += `<span class='float-right'>{% trans "Category ID" %}: ${data.pk}</span>`;
if (data.pathstring) {
html += `<p><small>${data.pathstring}</small></p>`;
}
return html;
}
// Rendered for "SupplierPart" model
function renderSupplierPart(name, data, parameters, options) {
var image = data.supplier_detail.image;
if (!image) {
image = `/static/img/blank_image.png`;
}
var html = `<img src='${image}' class='select2-thumbnail'>`;
html += ` <span><b>${data.supplier_detail.name}</b> - ${data.SKU}</span>`;
html += ` - <i>${data.part_detail.full_name}</i>`;
html += `<span class='float-right'>{% trans "Supplier Part ID" %}: ${data.pk}</span>`;
return html;
}

View File

@ -1,6 +1,68 @@
{% load i18n %}
{% load inventree_extras %}
// Create a new SalesOrder
function createSalesOrder(options={}) {
constructForm('{% url "api-so-list" %}', {
method: 'POST',
fields: {
reference: {
prefix: '{% settings_value "SALESORDER_REFERENCE_PREFIX" %}',
},
customer: {
value: options.customer,
},
description: {},
target_date: {
icon: 'fa-calendar-alt',
},
link: {
icon: 'fa-link',
},
responsible: {
icon: 'fa-user',
}
},
onSuccess: function(data) {
location.href = `/order/sales-order/${data.pk}/`;
},
title: '{% trans "Create Sales Order" %}',
});
}
// Create a new PurchaseOrder
function createPurchaseOrder(options={}) {
constructForm('{% url "api-po-list" %}', {
method: 'POST',
fields: {
reference: {
prefix: "{% settings_value 'PURCHASEORDER_REFERENCE_PREFIX' %}",
},
supplier: {
value: options.supplier,
},
description: {},
target_date: {
icon: 'fa-calendar-alt',
},
link: {
icon: 'fa-link',
},
responsible: {
icon: 'fa-user',
}
},
onSuccess: function(data) {
location.href = `/order/purchase-order/${data.pk}/`;
},
title: '{% trans "Create Purchase Order" %}',
});
}
function removeOrderRowFromOrderWizard(e) {
/* Remove a part selection from an order form. */
@ -266,6 +328,11 @@ function loadSalesOrderTable(table, options) {
field: 'customer_detail',
title: '{% trans "Customer" %}',
formatter: function(value, row, index, field) {
if (!row.customer_detail) {
return '{% trans "Invalid Customer" %}';
}
return imageHoverIcon(row.customer_detail.image) + renderLink(row.customer_detail.name, `/company/${row.customer}/sales-orders/`);
}
},

View File

@ -7,7 +7,7 @@
</div>
<div class='modal fade modal-fixed-footer modal-primary' tabindex='-1' role='dialog' id='modal-form'>
<div class='modal fade modal-fixed-footer modal-primary' role='dialog' id='modal-form'>
<div class='modal-dialog'>
<div class='modal-content'>
<div class="modal-header">
@ -33,7 +33,7 @@
</div>
</div>
<div class='modal fade modal-fixed-footer modal-secondary' tabindex='-1' role='dialog' id='modal-form-secondary'>
<div class='modal fade modal-fixed-footer modal-secondary' role='dialog' id='modal-form-secondary'>
<div class='modal-dialog'>
<div class='modal-content'>
<div class="modal-header">
@ -59,7 +59,7 @@
</div>
<div class='modal fade modal-fixed-footer' tabindex='-1' role='dialog' id='modal-question-dialog'>
<div class='modal fade modal-fixed-footer' role='dialog' id='modal-question-dialog'>
<div class='modal-dialog'>
<div class='modal-content'>
<div class="modal-header">
@ -79,7 +79,7 @@
</div>
</div>
<div class='modal fade modal-fixed-footer' tabindex='-1' role='dialog' id='modal-alert-dialog'>
<div class='modal fade modal-fixed-footer' role='dialog' id='modal-alert-dialog'>
<div class='modal-dialog'>
<div class='modal-content'>
<div class="modal-header">