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 # Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators # 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.conf.urls import url, include
from django.contrib import admin from django.contrib import admin
from django.contrib.auth import views as auth_views from django.contrib.auth import views as auth_views
from qr_code import urls as qr_code_urls
from company.urls import company_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'^logout/', auth_views.LogoutView.as_view(template_name='registration/logout.html'), name='logout'),
url(r'^admin/', admin.site.urls, name='inventree-admin'), 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'^index/', IndexView.as_view(), name='index'),
url(r'^search/', SearchView.as_view(), name='search'), 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.http import JsonResponse
from django.views import View 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 django.views.generic.base import TemplateView
from part.models import Part from part.models import Part
@ -144,6 +144,43 @@ class AjaxView(AjaxMixin, View):
return self.renderJsonResponse(request) 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): class AjaxCreateView(AjaxMixin, CreateView):
""" An 'AJAXified' CreateView for creating a new object in the db """ An 'AJAXified' CreateView for creating a new object in the db

View File

@ -180,7 +180,6 @@ class Part(models.Model):
def __str__(self): def __str__(self):
return "{n} - {d}".format(n=self.name, d=self.description) return "{n} - {d}".format(n=self.name, d=self.description)
@property
def format_barcode(self): def format_barcode(self):
""" Return a JSON string for formatting a barcode for this Part object """ """ Return a JSON string for formatting a barcode for this Part object """

View File

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

View File

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

View File

@ -34,8 +34,9 @@ part_attachment_urls = [
part_detail_urls = [ part_detail_urls = [
url(r'^edit/?', views.PartEdit.as_view(), name='part-edit'), url(r'^edit/?', views.PartEdit.as_view(), name='part-edit'),
url(r'^delete/?', views.PartDelete.as_view(), name='part-delete'), 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'^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'^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'^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'), 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'^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'^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'^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'), url(r'^thumbnail/?', views.PartImage.as_view(), name='part-image'),

View File

@ -27,6 +27,7 @@ from .forms import BomExportForm
from .forms import EditSupplierPartForm from .forms import EditSupplierPartForm
from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView
from InvenTree.views import QRCodeView
from InvenTree.helpers import DownloadFile, str2bool from InvenTree.helpers import DownloadFile, str2bool
@ -234,6 +235,21 @@ class PartDetail(DetailView):
return context 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): class PartImage(AjaxUpdateView):
""" View for uploading Part image """ """ 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) { function modalEnable(modal, enable=true) {
/* Enable (or disable) modal form elements to prevent user input /* Enable (or disable) modal form elements to prevent user input
@ -444,6 +455,14 @@ function launchModalForm(url, options = {}) {
* an object called 'html_form' * an object called 'html_form'
* *
* If the request is NOT successful, displays an appropriate error message. * 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'; var modal = options.modal || '#modal-form';
@ -475,7 +494,13 @@ function launchModalForm(url, options = {}) {
if (response.html_form) { if (response.html_form) {
injectModalForm(modal, 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 { } else {
$(modal).modal('hide'); $(modal).modal('hide');

View File

@ -36,7 +36,6 @@ class StockLocation(InvenTreeTree):
def has_items(self): def has_items(self):
return self.stock_items.count() > 0 return self.stock_items.count() > 0
@property
def format_barcode(self): def format_barcode(self):
""" Return a JSON string for formatting a barcode for this StockLocation object """ """ Return a JSON string for formatting a barcode for this StockLocation object """
@ -139,7 +138,6 @@ class StockItem(models.Model):
('part', 'serial'), ('part', 'serial'),
] ]
@property
def format_barcode(self): def format_barcode(self):
""" Return a JSON string for formatting a barcode for this StockItem. """ Return a JSON string for formatting a barcode for this StockItem.
Can be used to perform lookup of a stockitem using barcode Can be used to perform lookup of a stockitem using barcode

View File

@ -2,8 +2,6 @@
{% load static %} {% load static %}
{% block content %} {% block content %}
{% load qr_code %}
<div class='row'> <div class='row'>
<div class='col-sm-6'> <div class='col-sm-6'>
<h3>Stock Item Details</h3> <h3>Stock Item Details</h3>
@ -25,6 +23,8 @@
<li><a href='#' id='stock-stocktake' title='Count stock'>Stocktake</a></li> <li><a href='#' id='stock-stocktake' title='Count stock'>Stocktake</a></li>
{% endif %} {% endif %}
<li><a href="#" id='stock-delete' title='Delete stock item'>Delete stock item</a></li> <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> </ul>
</div> </div>
</div> </div>
@ -109,9 +109,6 @@
{% endif %} {% endif %}
</table> </table>
</div> </div>
<div class='col-sm-6'>
{% qr_from_text item.format_barcode size="s" image_format="png" error_correction="L" %}
</div>
</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 %} {% if item.in_stock %}
$("#stock-move").click(function() { $("#stock-move").click(function() {
launchModalForm( launchModalForm(

View File

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

View File

@ -10,6 +10,7 @@ from . import views
stock_location_detail_urls = [ stock_location_detail_urls = [
url(r'^edit/?', views.StockLocationEdit.as_view(), name='stock-location-edit'), url(r'^edit/?', views.StockLocationEdit.as_view(), name='stock-location-edit'),
url(r'^delete/?', views.StockLocationDelete.as_view(), name='stock-location-delete'), 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 # Anything else
url('^.*$', views.StockLocationDetail.as_view(), name='stock-location-detail'), 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'^delete/?', views.StockItemDelete.as_view(), name='stock-item-delete'),
url(r'^move/?', views.StockItemMove.as_view(), name='stock-item-move'), url(r'^move/?', views.StockItemMove.as_view(), name='stock-item-move'),
url(r'^stocktake/?', views.StockItemStocktake.as_view(), name='stock-item-stocktake'), 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'), 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 django.forms import HiddenInput
from InvenTree.views import AjaxUpdateView, AjaxDeleteView, AjaxCreateView from InvenTree.views import AjaxUpdateView, AjaxDeleteView, AjaxCreateView
from InvenTree.views import QRCodeView
from part.models import Part from part.models import Part
from .models import StockItem, StockLocation, StockItemTracking from .models import StockItem, StockLocation, StockItemTracking
@ -75,6 +76,34 @@ class StockLocationEdit(AjaxUpdateView):
ajax_form_title = 'Edit Stock Location' 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): class StockItemEdit(AjaxUpdateView):
""" """
View for editing details of a single StockItem 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>