Merge pull request #694 from SchrodingersGat/thumbnail-image

Thumbnail image
This commit is contained in:
Oliver 2020-04-05 00:55:32 +11:00 committed by GitHub
commit cffb921fb1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 146 additions and 15 deletions

View File

@ -5,6 +5,8 @@ import logging
import time import time
import operator import operator
from rest_framework.authtoken.models import Token
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -20,10 +22,49 @@ class AuthRequiredMiddleware(object):
response = self.get_response(request) response = self.get_response(request)
# Redirect any unauthorized HTTP requests to the login page
if not request.user.is_authenticated: if not request.user.is_authenticated:
if not request.path_info == reverse_lazy('login') and not request.path_info.startswith('/api/'): """
return HttpResponseRedirect(reverse_lazy('login')) Normally, a web-based session would use csrftoken based authentication.
However when running an external application (e.g. the InvenTree app),
we wish to use token-based auth to grab media files.
So, we will allow token-based authentication but ONLY for the /media/ directory.
What problem is this solving?
- The InvenTree mobile app does not use csrf token auth
- Token auth is used by the Django REST framework, but that is under the /api/ endpoint
- Media files (e.g. Part images) are required to be served to the app
- We do not want to make /media/ files accessible without login!
There is PROBABLY a better way of going about this?
a) Allow token-based authentication against a user?
b) Serve /media/ files in a duplicate location e.g. /api/media/ ?
c) Is there a "standard" way of solving this problem?
My [google|stackoverflow]-fu has failed me. So this hack has been created.
"""
authorized = False
if 'Authorization' in request.headers.keys():
auth = request.headers['Authorization'].strip()
if auth.startswith('Token') and len(auth.split()) == 2:
token = auth.split()[1]
# Does the provided token match a valid user?
if Token.objects.filter(key=token).exists():
allowed = ['/media/', '/static/']
# Only allow token-auth for /media/ or /static/ dirs!
if any([request.path_info.startswith(a) for a in allowed]):
authorized = True
# No authorization was found for the request
if not authorized:
if not request.path_info == reverse_lazy('login') and not request.path_info.startswith('/api/'):
return HttpResponseRedirect(reverse_lazy('login'))
# Code to be executed for each request/response after # Code to be executed for each request/response after
# the view is called. # the view is called.
@ -38,6 +79,8 @@ class QueryCountMiddleware(object):
status code of 200). It does not currently support status code of 200). It does not currently support
multi-db setups. multi-db setups.
To enable this middleware, set 'log_queries: True' in the local InvenTree config file.
Reference: https://www.dabapps.com/blog/logging-sql-queries-django-13/ Reference: https://www.dabapps.com/blog/logging-sql-queries-django-13/
""" """

View File

@ -139,7 +139,7 @@ function loadPartTable(table, url, options={}) {
name = '<i>' + name + '</i>'; name = '<i>' + name + '</i>';
} }
var display = imageHoverIcon(row.image) + renderLink(name, '/part/' + row.pk + '/'); var display = imageHoverIcon(row.thumbnail) + renderLink(name, '/part/' + row.pk + '/');
if (row.is_template) { if (row.is_template) {
display = display + "<span class='label label-info' style='float: right;'>TEMPLATE</span>"; display = display + "<span class='label label-info' style='float: right;'>TEMPLATE</span>";

View File

@ -70,7 +70,7 @@ function loadStockTable(table, options) {
name += row.part__name; name += row.part__name;
return imageHoverIcon(row.part__image) + name + ' <i>(' + data.length + ' items)</i>'; return imageHoverIcon(row.part__thumbnail) + name + ' <i>(' + data.length + ' items)</i>';
} }
else if (field == 'part__description') { else if (field == 'part__description') {
return row.part__description; return row.part__description;
@ -188,7 +188,7 @@ function loadStockTable(table, options) {
name += row.part__revision; name += row.part__revision;
} }
return imageHoverIcon(row.part__image) + renderLink(name, '/part/' + row.part + '/stock/'); return imageHoverIcon(row.part__thumbnail) + renderLink(name, '/part/' + row.part + '/stock/');
} }
}, },
{ {

View File

@ -109,9 +109,8 @@ urlpatterns = [
# Static file access # Static file access
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
if settings.DEBUG: # Media file access
# Media file access urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# Send any unknown URLs to the parts page # Send any unknown URLs to the parts page
urlpatterns += [url(r'^.*$', RedirectView.as_view(url='/index/', permanent=False), name='index')] urlpatterns += [url(r'^.*$', RedirectView.as_view(url='/index/', permanent=False), name='index')]

View File

@ -221,7 +221,17 @@ class PartList(generics.ListCreateAPIView):
for item in data: for item in data:
if item['image']: if item['image']:
item['image'] = os.path.join(settings.MEDIA_URL, item['image']) img = item['image']
# Use the 'thumbnail' image here instead of the full-size image
# Note: The full-size image is used when requesting the /api/part/<x>/ endpoint
fn, ext = os.path.splitext(img)
thumb = "{fn}.thumbnail{ext}".format(fn=fn, ext=ext)
item['thumbnail'] = os.path.join(settings.MEDIA_URL, thumb)
del item['image']
cat_id = item['category'] cat_id = item['category']

View File

@ -0,0 +1,20 @@
# Generated by Django 2.2.10 on 2020-04-04 04:45
from django.db import migrations
import part.models
import stdimage.models
class Migration(migrations.Migration):
dependencies = [
('part', '0032_auto_20200322_0453'),
]
operations = [
migrations.AlterField(
model_name='part',
name='image',
field=stdimage.models.StdImageField(blank=True, null=True, upload_to=part.models.rename_part_image),
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 2.2.10 on 2020-04-04 12:38
from django.db import migrations
from part.models import Part
from stdimage.utils import render_variations
def create_thumbnails(apps, schema_editor):
"""
Create thumbnails for all existing Part images.
"""
for part in Part.objects.all():
# Render thumbnail for each existing Part
if part.image:
part.image.render_variations()
class Migration(migrations.Migration):
dependencies = [
('part', '0033_auto_20200404_0445'),
]
operations = [
migrations.RunPython(create_thumbnails),
]

View File

@ -27,6 +27,8 @@ from django_cleanup import cleanup
from mptt.models import TreeForeignKey from mptt.models import TreeForeignKey
from stdimage.models import StdImageField
from decimal import Decimal from decimal import Decimal
from datetime import datetime from datetime import datetime
from rapidfuzz import fuzz from rapidfuzz import fuzz
@ -302,6 +304,16 @@ class Part(models.Model):
else: else:
return os.path.join(settings.STATIC_URL, 'img/blank_image.png') return os.path.join(settings.STATIC_URL, 'img/blank_image.png')
def get_thumbnail_url(self):
"""
Return the URL of the image thumbnail for this part
"""
if self.image:
return os.path.join(settings.MEDIA_URL, str(self.image.thumbnail.url))
else:
return os.path.join(settings.STATIC_URL, 'img/blank_image.thumbnail.png')
def validate_unique(self, exclude=None): def validate_unique(self, exclude=None):
""" Validate that a part is 'unique'. """ Validate that a part is 'unique'.
Uniqueness is checked across the following (case insensitive) fields: Uniqueness is checked across the following (case insensitive) fields:
@ -373,7 +385,13 @@ class Part(models.Model):
URL = InvenTreeURLField(blank=True, help_text=_('Link to extenal URL')) URL = InvenTreeURLField(blank=True, help_text=_('Link to extenal URL'))
image = models.ImageField(upload_to=rename_part_image, max_length=255, null=True, blank=True) image = StdImageField(
upload_to=rename_part_image,
null=True,
blank=True,
variations={'thumbnail': (128, 128)},
delete_orphans=True,
)
default_location = TreeForeignKey('stock.StockLocation', on_delete=models.SET_NULL, default_location = TreeForeignKey('stock.StockLocation', on_delete=models.SET_NULL,
blank=True, null=True, blank=True, null=True,

View File

@ -47,7 +47,7 @@ class PartBriefSerializer(InvenTreeModelSerializer):
""" Serializer for Part (brief detail) """ """ Serializer for Part (brief detail) """
url = serializers.CharField(source='get_absolute_url', read_only=True) url = serializers.CharField(source='get_absolute_url', read_only=True)
image_url = serializers.CharField(source='get_image_url', read_only=True) image_url = serializers.CharField(source='get_thumbnail_url', read_only=True)
@staticmethod @staticmethod
def setup_eager_loading(queryset): def setup_eager_loading(queryset):
@ -79,7 +79,8 @@ class PartSerializer(InvenTreeModelSerializer):
""" """
url = serializers.CharField(source='get_absolute_url', read_only=True) url = serializers.CharField(source='get_absolute_url', read_only=True)
image_url = serializers.CharField(source='get_image_url', read_only=True) image = serializers.CharField(source='get_image_url', read_only=True)
thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True)
category_name = serializers.CharField(source='category_path', read_only=True) category_name = serializers.CharField(source='category_path', read_only=True)
allocated_stock = serializers.IntegerField(source='allocation_count', read_only=True) allocated_stock = serializers.IntegerField(source='allocation_count', read_only=True)
@ -100,7 +101,8 @@ class PartSerializer(InvenTreeModelSerializer):
'url', # Link to the part detail page 'url', # Link to the part detail page
'category', 'category',
'category_name', 'category_name',
'image_url', 'image',
'thumbnail',
'full_name', 'full_name',
'name', 'name',
'IPN', 'IPN',

View File

@ -337,7 +337,17 @@ class StockList(generics.ListCreateAPIView):
locations = {} locations = {}
for item in data: for item in data:
item['part__image'] = os.path.join(settings.MEDIA_URL, item['part__image'])
img = item['part__image']
# Use the thumbnail image instead
fn, ext = os.path.splitext(img)
thumb = "{fn}.thumbnail{ext}".format(fn=fn, ext=ext)
item['part__thumbnail'] = os.path.join(settings.MEDIA_URL, thumb)
del item['part__image']
loc_id = item['location'] loc_id = item['location']

View File

@ -18,3 +18,4 @@ flake8==3.3.0 # PEP checking
coverage==4.0.3 # Unit test coverage coverage==4.0.3 # Unit test coverage
python-coveralls==2.9.1 # Coveralls linking (for Travis) python-coveralls==2.9.1 # Coveralls linking (for Travis)
rapidfuzz==0.2.1 # Fuzzy string matching rapidfuzz==0.2.1 # Fuzzy string matching
django-stdimage==5.0.3 # Advanced ImageField management