mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #694 from SchrodingersGat/thumbnail-image
Thumbnail image
This commit is contained in:
commit
cffb921fb1
@ -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/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -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>";
|
||||||
|
@ -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/');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -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')]
|
||||||
|
@ -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']
|
||||||
|
|
||||||
|
20
InvenTree/part/migrations/0033_auto_20200404_0445.py
Normal file
20
InvenTree/part/migrations/0033_auto_20200404_0445.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
28
InvenTree/part/migrations/0034_auto_20200404_1238.py
Normal file
28
InvenTree/part/migrations/0034_auto_20200404_1238.py
Normal 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),
|
||||||
|
]
|
@ -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,
|
||||||
|
@ -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',
|
||||||
|
@ -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']
|
||||||
|
|
||||||
|
@ -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
|
Loading…
Reference in New Issue
Block a user