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 operator
from rest_framework.authtoken.models import Token
logger = logging.getLogger(__name__)
@ -20,10 +22,49 @@ class AuthRequiredMiddleware(object):
response = self.get_response(request)
# Redirect any unauthorized HTTP requests to the login page
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
# the view is called.
@ -38,6 +79,8 @@ class QueryCountMiddleware(object):
status code of 200). It does not currently support
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/
"""

View File

@ -139,7 +139,7 @@ function loadPartTable(table, url, options={}) {
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) {
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;
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') {
return row.part__description;
@ -188,7 +188,7 @@ function loadStockTable(table, options) {
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
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
if settings.DEBUG:
# Media file access
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# Media file access
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# Send any unknown URLs to the parts page
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:
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']

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 stdimage.models import StdImageField
from decimal import Decimal
from datetime import datetime
from rapidfuzz import fuzz
@ -302,6 +304,16 @@ class Part(models.Model):
else:
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):
""" Validate that a part is 'unique'.
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'))
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,
blank=True, null=True,

View File

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

View File

@ -337,7 +337,17 @@ class StockList(generics.ListCreateAPIView):
locations = {}
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']

View File

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