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 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/
|
||||
"""
|
||||
|
||||
|
@ -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>";
|
||||
|
@ -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/');
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -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')]
|
||||
|
@ -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']
|
||||
|
||||
|
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 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,
|
||||
|
@ -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',
|
||||
|
@ -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']
|
||||
|
||||
|
@ -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
|
Loading…
Reference in New Issue
Block a user