QR code improvements

- Display QR codes as links to served images
- The qr_code plugin caches these images in the background
- Make a qr_code template to push out as a modal form
- Create a QRCodeView to simplify display of QR codes
- Add option to launchModalForm() to disable the 'submit' button

Refactored QR code display for

- StockLocation
- StockItem
- Part
This commit is contained in:
Oliver Walters 2019-05-04 18:46:57 +10:00
parent 8e65c0a120
commit 9aa1a70f18
15 changed files with 171 additions and 21 deletions

View File

@ -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

View File

@ -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'),

View File

@ -12,7 +12,7 @@ from django.template.loader import render_to_string
from django.http import JsonResponse
from django.views import View
from django.views.generic import UpdateView, CreateView, DeleteView
from django.views.generic import UpdateView, CreateView, DeleteView, DetailView
from django.views.generic.base import TemplateView
from part.models import Part
@ -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

View File

@ -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 """

View File

@ -1,6 +1,5 @@
{% extends "part/part_base.html" %}
{% load static %}
{% load qr_code %}
{% block details %}
{% include 'part/tabs.html' with tab='detail' %}
@ -24,6 +23,7 @@
{% else %}
<li><a href="#" id='activate-part' title='Activate part'>Activate</a></li>
{% endif %}
<li><a href='#' id='show-qr-code' title='Generate QR Code'>Show QR Code</a></li>
</ul>
</div>
</h3>
@ -116,8 +116,6 @@
</div>
{% endif %}
{% qr_from_text part.format_barcode size="s" image_format="png" error_correction="L" %}
{% endblock %}
{% block js_load %}
@ -129,6 +127,15 @@
{% block js_ready %}
{{ block.super }}
$("#show-qr-code").click(function() {
launchModalForm(
"{% url 'part-qr' part.id %}",
{
no_post: true,
}
);
});
$("#duplicate-part").click(function() {
launchModalForm(
"{% url 'part-create' %}",

View File

@ -22,9 +22,7 @@
</div>
<div class="media-body">
<h4>{{ part.name }}{% if part.active == False %} <i>- INACTIVE</i>{% endif %}</h4>
{% if part.description %}
<p><i>{{ part.description }}</i></p>
{% endif %}
{% if part.IPN %}
<tr>
<td>IPN</td>

View File

@ -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'),
@ -44,6 +45,8 @@ part_detail_urls = [
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'),
# Any other URLs go to the part detail page

View File

@ -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 """

View File

@ -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
@ -444,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';
@ -475,7 +494,13 @@ function launchModalForm(url, options = {}) {
if (response.html_form) {
injectModalForm(modal, response.html_form);
if (options.no_post) {
modalShowSubmitButton(modal, false);
} else {
modalShowSubmitButton(modal, true);
handleModalForm(url, options);
}
} else {
$(modal).modal('hide');

View File

@ -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

View File

@ -2,8 +2,6 @@
{% load static %}
{% block content %}
{% load qr_code %}
<div class='row'>
<div class='col-sm-6'>
<h3>Stock Item Details</h3>
@ -25,6 +23,8 @@
<li><a href='#' id='stock-stocktake' title='Count stock'>Stocktake</a></li>
{% endif %}
<li><a href="#" id='stock-delete' title='Delete stock item'>Delete stock item</a></li>
<hr>
<li><a href="#" id='item-qr-code' title='Generate QR code'>Show QR code</a></li>
</ul>
</div>
</div>
@ -109,9 +109,6 @@
{% endif %}
</table>
</div>
<div class='col-sm-6'>
{% qr_from_text item.format_barcode size="s" image_format="png" error_correction="L" %}
</div>
</div>
@ -148,6 +145,13 @@
});
});
$("#item-qr-code").click(function() {
launchModalForm("{% url 'stock-item-qr' item.id %}",
{
no_post: true,
});
});
{% if item.in_stock %}
$("#stock-move").click(function() {
launchModalForm(

View File

@ -1,6 +1,5 @@
{% extends "stock/stock_app_base.html" %}
{% load static %}
{% load qr_code %}
{% block content %}
<div class='row'>
@ -24,9 +23,10 @@
<ul class="dropdown-menu">
<li><a href="#" id='location-edit' title='Edit stock location'>Edit</a></li>
<li><a href="#" id='location-delete' title='Delete stock location'>Delete</a></li>
<hr>
<li><a href="#" id='location-qr-code' title='Generate QR code'>Show QR code</a></li>
</ul>
</div>
{% qr_from_text location.format_barcode size="s" image_format="png" error_correction="L" %}
{% endif %}
</div>
</h3>
@ -101,6 +101,13 @@
return false;
});
$('#location-qr-code').click(function() {
launchModalForm("{% url 'stock-location-qr' location.id %}",
{
no_post: true,
});
});
{% endif %}
$('#item-create').click(function () {
@ -170,5 +177,4 @@
},
url: "{% url 'api-stock-list' %}",
});
{% endblock %}

View File

@ -10,6 +10,7 @@ from . import views
stock_location_detail_urls = [
url(r'^edit/?', views.StockLocationEdit.as_view(), name='stock-location-edit'),
url(r'^delete/?', views.StockLocationDelete.as_view(), name='stock-location-delete'),
url(r'^qr_code/?', views.StockLocationQRCode.as_view(), name='stock-location-qr'),
# Anything else
url('^.*$', views.StockLocationDetail.as_view(), name='stock-location-detail'),
@ -20,6 +21,7 @@ stock_item_detail_urls = [
url(r'^delete/?', views.StockItemDelete.as_view(), name='stock-item-delete'),
url(r'^move/?', views.StockItemMove.as_view(), name='stock-item-move'),
url(r'^stocktake/?', views.StockItemStocktake.as_view(), name='stock-item-stocktake'),
url(r'^qr_code/?', views.StockItemQRCode.as_view(), name='stock-item-qr'),
url('^.*$', views.StockItemDetail.as_view(), name='stock-item-detail'),
]

View File

@ -10,6 +10,7 @@ from django.forms.models import model_to_dict
from django.forms import HiddenInput
from InvenTree.views import AjaxUpdateView, AjaxDeleteView, AjaxCreateView
from InvenTree.views import QRCodeView
from part.models import Part
from .models import StockItem, StockLocation, StockItemTracking
@ -75,6 +76,34 @@ class StockLocationEdit(AjaxUpdateView):
ajax_form_title = 'Edit Stock Location'
class StockLocationQRCode(QRCodeView):
""" View for displaying a QR code for a StockLocation object """
ajax_form_title = "Stock Location QR code"
def get_qr_data(self):
""" Generate QR code data for the StockLocation """
try:
loc = StockLocation.objects.get(id=self.pk)
return loc.format_barcode()
except StockLocation.DoesNotExist:
return None
class StockItemQRCode(QRCodeView):
""" View for displaying a QR code for a StockItem object """
ajax_form_title = "Stock Item QR Code"
def get_qr_data(self):
""" Generate QR code data for the StockItem """
try:
item = StockItem.objects.get(id=self.pk)
return item.format_barcode()
except StockItem.DoesNotExist:
return None
class StockItemEdit(AjaxUpdateView):
"""
View for editing details of a single StockItem

View File

@ -0,0 +1,10 @@
{% load qr_code %}
<div class='container' style='width: 80%;'>
{% if qr_data %}
<img src="{% qr_url_from_text qr_data size='m' error_correction='q' %}" alt="QR Code">
{% else %}
<b>Error:</b><br>
{{ error_msg }}
{% endif %}
</div>