diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index 61fb50831b..c9c9e2966b 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -23,9 +23,16 @@ class InvenTreeTree(models.Model): abstract = True unique_together = ('name', 'parent') - name = models.CharField(max_length=100, unique=True) + name = models.CharField( + blank=False, + max_length=100, + unique=True + ) - description = models.CharField(max_length=250) + description = models.CharField( + blank=False, + max_length=250 + ) # When a category is deleted, graft the children onto its parent parent = models.ForeignKey('self', diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 82c8a99507..fbced0fda7 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -124,6 +124,19 @@ DATABASES = { } } +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + }, + 'qr-code': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + 'LOCATION': 'qr-code-cache', + 'TIMEOUT': 3600 + } +} + +QR_CODE_CACHE_ALIAS = 'qr-code' + # Password validation # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index ccc828de55..4038c2870a 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -8,6 +8,7 @@ Passes URL lookup downstream to each app as required. from django.conf.urls import url, include from django.contrib import admin from django.contrib.auth import views as auth_views +from qr_code import urls as qr_code_urls from company.urls import company_urls @@ -62,6 +63,8 @@ urlpatterns = [ url(r'^logout/', auth_views.LogoutView.as_view(template_name='registration/logout.html'), name='logout'), url(r'^admin/', admin.site.urls, name='inventree-admin'), + url(r'^qr_code/', include(qr_code_urls, namespace='qr_code')), + url(r'^index/', IndexView.as_view(), name='index'), url(r'^search/', SearchView.as_view(), name='search'), diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index e727063f0f..15926715f6 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -144,6 +144,43 @@ class AjaxView(AjaxMixin, View): return self.renderJsonResponse(request) +class QRCodeView(AjaxView): + """ An 'AJAXified' view for displaying a QR code. + + Subclasses should implement the get_qr_data(self) function. + """ + + ajax_template_name = "qr_code.html" + + def get(self, request, *args, **kwargs): + self.request = request + self.pk = self.kwargs['pk'] + return self.renderJsonResponse(request, None, context=self.get_context_data()) + + def get_qr_data(self): + """ Returns the text object to render to a QR code. + The actual rendering will be handled by the template """ + + return None + + def get_context_data(self): + """ Get context data for passing to the rendering template. + + Explicity passes the parameter 'qr_data' + """ + + context = {} + + qr = self.get_qr_data() + + if qr: + context['qr_data'] = qr + else: + context['error_msg'] = 'Error generating QR code' + + return context + + class AjaxCreateView(AjaxMixin, CreateView): """ An 'AJAXified' CreateView for creating a new object in the db diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index a25c99f37d..7175a18650 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -87,7 +87,10 @@ class Build(models.Model): limit_choices_to={'buildable': True}, ) - title = models.CharField(max_length=100, help_text='Brief description of the build') + title = models.CharField( + blank=False, + max_length=100, + help_text='Brief description of the build') quantity = models.PositiveIntegerField( default=1, diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index dcee37959d..1d119ddf51 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -42,7 +42,7 @@ class Company(models.Model): It may be a supplier or a customer (or both). """ - name = models.CharField(max_length=100, unique=True, + name = models.CharField(max_length=100, blank=False, unique=True, help_text='Company name') description = models.CharField(max_length=500) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 9f5d9f0e29..e2823ba93b 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -180,7 +180,6 @@ class Part(models.Model): def __str__(self): return "{n} - {d}".format(n=self.name, d=self.description) - @property def format_barcode(self): """ Return a JSON string for formatting a barcode for this Part object """ diff --git a/InvenTree/part/templates/part/category.html b/InvenTree/part/templates/part/category.html index f199cc5981..d03d18be44 100644 --- a/InvenTree/part/templates/part/category.html +++ b/InvenTree/part/templates/part/category.html @@ -41,7 +41,15 @@
IPN | diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index 401a038193..4cda10a0f0 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -34,8 +34,9 @@ part_attachment_urls = [ part_detail_urls = [ url(r'^edit/?', views.PartEdit.as_view(), name='part-edit'), url(r'^delete/?', views.PartDelete.as_view(), name='part-delete'), - url(r'^track/?', views.PartDetail.as_view(template_name='part/track.html'), name='part-track'), url(r'^bom-export/?', views.BomDownload.as_view(), name='bom-export'), + + url(r'^track/?', views.PartDetail.as_view(template_name='part/track.html'), name='part-track'), url(r'^attachments/?', views.PartDetail.as_view(template_name='part/attachments.html'), name='part-attachments'), url(r'^bom/?', views.PartDetail.as_view(template_name='part/bom.html'), name='part-bom'), url(r'^build/?', views.PartDetail.as_view(template_name='part/build.html'), name='part-build'), @@ -43,6 +44,8 @@ part_detail_urls = [ url(r'^used/?', views.PartDetail.as_view(template_name='part/used_in.html'), name='part-used-in'), url(r'^allocation/?', views.PartDetail.as_view(template_name='part/allocation.html'), name='part-allocation'), url(r'^suppliers/?', views.PartDetail.as_view(template_name='part/supplier.html'), name='part-suppliers'), + + url(r'^qr_code/?', views.PartQRCode.as_view(), name='part-qr'), url(r'^thumbnail/?', views.PartImage.as_view(), name='part-image'), diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 27d52dcdb7..d14827a446 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -27,6 +27,7 @@ from .forms import BomExportForm from .forms import EditSupplierPartForm from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView +from InvenTree.views import QRCodeView from InvenTree.helpers import DownloadFile, str2bool @@ -234,6 +235,21 @@ class PartDetail(DetailView): return context +class PartQRCode(QRCodeView): + """ View for displaying a QR code for a Part object """ + + ajax_form_title = "Part QR Code" + + def get_qr_data(self): + """ Generate QR code data for the Part """ + + try: + part = Part.objects.get(id=self.pk) + return part.format_barcode() + except Part.DoesNotExist: + return None + + class PartImage(AjaxUpdateView): """ View for uploading Part image """ @@ -395,6 +411,7 @@ class CategoryDelete(AjaxDeleteView): """ Delete view to delete a PartCategory """ model = PartCategory ajax_template_name = 'part/category_delete.html' + ajax_form_title = 'Delete Part Category' context_object_name = 'category' success_url = '/part/' diff --git a/InvenTree/static/css/inventree.css b/InvenTree/static/css/inventree.css index 0a1c34036b..98a4e04de4 100644 --- a/InvenTree/static/css/inventree.css +++ b/InvenTree/static/css/inventree.css @@ -152,3 +152,18 @@ .part-allocation-overallocated { background: #ccf5ff; } + +.glyphicon-refresh-animate { + -animation: spin .7s infinite linear; + -webkit-animation: spin2 .7s infinite linear; +} + +@-webkit-keyframes spin2 { + from { -webkit-transform: rotate(0deg);} + to { -webkit-transform: rotate(360deg);} +} + +@keyframes spin { + from { transform: scale(1) rotate(0deg);} + to { transform: scale(1) rotate(360deg);} +} \ No newline at end of file diff --git a/InvenTree/static/script/inventree/modals.js b/InvenTree/static/script/inventree/modals.js index c16772704a..bc7228063f 100644 --- a/InvenTree/static/script/inventree/modals.js +++ b/InvenTree/static/script/inventree/modals.js @@ -24,7 +24,7 @@ function loadingMessageContent() { */ // TODO - This can be made a lot better - return 'Loading...'; + return " Waiting for server..."; } @@ -73,6 +73,17 @@ function afterForm(response, options) { } } +function modalShowSubmitButton(modal, show=true) { + /* Show (or hide) the 'Submit' button for the given modal form + */ + + if (show) { + $(modal).find('#modal-form-submit').show(); + } else { + $(modal).find('#modal-form-submit').hide(); + } +} + function modalEnable(modal, enable=true) { /* Enable (or disable) modal form elements to prevent user input @@ -155,16 +166,16 @@ function renderErrorMessage(xhr) { } -function showDialog(title, content, options={}) { +function showAlertDialog(title, content, options={}) { /* Display a modal dialog message box. * * title - Title text * content - HTML content of the dialog window * options: - * modal - modal form to use (default = '#modal-dialog') + * modal - modal form to use (default = '#modal-alert-dialog') */ - var modal = options.modal || '#modal-dialog'; + var modal = options.modal || '#modal-alert-dialog'; $(modal).on('shown.bs.modal', function() { $(modal + ' .modal-form-content').scrollTop(0); @@ -181,6 +192,54 @@ function showDialog(title, content, options={}) { $(modal).modal('show'); } + +function showQuestionDialog(title, content, options={}) { + /* Display a modal dialog for user input (Yes/No confirmation dialog) + * + * title - Title text + * content - HTML content of the dialog window + * options: + * modal - Modal target (default = 'modal-question-dialog') + * accept_text - Text for the accept button (default = 'Accept') + * cancel_text - Text for the cancel button (default = 'Cancel') + * accept - Function to run if the user presses 'Accept' + * cancel - Functino to run if the user presses 'Cancel' + */ + + var modal = options.modal || '#modal-question-dialog'; + + $(modal).on('shown.bs.modal', function() { + $(modal + ' .modal-form-content').scrollTop(0); + }); + + modalSetTitle(modal, title); + modalSetContent(modal, content); + + var accept_text = options.accept_text || 'Accept'; + var cancel_text = options.cancel_text || 'Cancel'; + + $(modal).find('#modal-form-cancel').html(cancel_text); + $(modal).find('#modal-form-accept').html(accept_text); + + $(modal).on('click', '#modal-form-accept', function() { + $(modal).modal('hide'); + + if (options.accept) { + options.accept(); + } + }); + + $(modal).on('click', 'modal-form-cancel', function() { + $(modal).modal('hide'); + + if (options.cancel) { + options.cancel(); + } + }); + + $(modal).modal('show'); +} + function openModal(options) { /* Open a modal form, and perform some action based on the provided options object: * @@ -215,7 +274,7 @@ function openModal(options) { if (options.title) { modalSetTitle(modal, options.title); } else { - modalSetTitle(modal, 'Loading Form Data...'); + modalSetTitle(modal, 'Loading Data...'); } // Unless the content is explicitly set, display loading message @@ -275,12 +334,12 @@ function launchDeleteForm(url, options = {}) { else { $(modal).modal('hide'); - showDialog('Invalid form response', 'JSON response missing HTML data'); + showAlertDialog('Invalid form response', 'JSON response missing HTML data'); } }, error: function (xhr, ajaxOptions, thrownError) { $(modal).modal('hide'); - showDialog('Error requesting form data', renderErrorMessage(xhr)); + showAlertDialog('Error requesting form data', renderErrorMessage(xhr)); } }); @@ -299,7 +358,7 @@ function launchDeleteForm(url, options = {}) { }, error: function (xhr, ajaxOptions, thrownError) { $(modal).modal('hide'); - showDialog('Error deleting item', renderErrorMessage(xhr)); + showAlertDialog('Error deleting item', renderErrorMessage(xhr)); } }); }); @@ -365,7 +424,7 @@ function handleModalForm(url, options) { } else { $(modal).modal('hide'); - showDialog('Invalid response from server', 'Form data missing from server response'); + showAlertDialog('Invalid response from server', 'Form data missing from server response'); } } } @@ -378,7 +437,7 @@ function handleModalForm(url, options) { // There was an error submitting form data via POST $(modal).modal('hide'); - showDialog('Error posting form data', renderErrorMessage(xhr)); + showAlertDialog('Error posting form data', renderErrorMessage(xhr)); }, complete: function(xhr) { //TODO @@ -396,6 +455,14 @@ function launchModalForm(url, options = {}) { * an object called 'html_form' * * If the request is NOT successful, displays an appropriate error message. + * + * options: + * + * modal - Name of the modal (default = '#modal-form') + * data - Data to pass through to the AJAX request to fill the form + * submit_text - Text for the submit button (default = 'Submit') + * close_text - Text for the close button (default = 'Close') + * no_post - If true, only display form data, hide submit button, and disallow POST */ var modal = options.modal || '#modal-form'; @@ -427,16 +494,22 @@ function launchModalForm(url, options = {}) { if (response.html_form) { injectModalForm(modal, response.html_form); - handleModalForm(url, options); + + if (options.no_post) { + modalShowSubmitButton(modal, false); + } else { + modalShowSubmitButton(modal, true); + handleModalForm(url, options); + } } else { $(modal).modal('hide'); - showDialog('Invalid server response', 'JSON response missing form data'); + showAlertDialog('Invalid server response', 'JSON response missing form data'); } }, error: function (xhr, ajaxOptions, thrownError) { $(modal).modal('hide'); - showDialog('Error requesting form data', renderErrorMessage(xhr)); + showAlertDialog('Error requesting form data', renderErrorMessage(xhr)); } }; diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 7cdc45b7bc..2c16fd079c 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -36,7 +36,6 @@ class StockLocation(InvenTreeTree): def has_items(self): return self.stock_items.count() > 0 - @property def format_barcode(self): """ Return a JSON string for formatting a barcode for this StockLocation object """ @@ -139,7 +138,6 @@ class StockItem(models.Model): ('part', 'serial'), ] - @property def format_barcode(self): """ Return a JSON string for formatting a barcode for this StockItem. Can be used to perform lookup of a stockitem using barcode @@ -390,7 +388,7 @@ class StockItemTracking(models.Model): date = models.DateTimeField(auto_now_add=True, editable=False) # Short-form title for this tracking entry - title = models.CharField(max_length=250) + title = models.CharField(blank=False, max_length=250) # Optional longer description notes = models.TextField(blank=True) diff --git a/InvenTree/stock/templates/stock/item.html b/InvenTree/stock/templates/stock/item.html index 30b7902886..ca38ea29ef 100644 --- a/InvenTree/stock/templates/stock/item.html +++ b/InvenTree/stock/templates/stock/item.html @@ -2,8 +2,6 @@ {% load static %} {% block content %} -{% load qr_code %} -