Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: increase coverage #8897

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/backend/InvenTree/InvenTree/helpers_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,16 +211,16 @@ def render_currency(
except Exception:
pass

if min_decimal_places is None:
if min_decimal_places is None or not isinstance(min_decimal_places, (int, float)):
min_decimal_places = get_global_setting('PRICING_DECIMAL_PLACES_MIN', 0)

if max_decimal_places is None:
if max_decimal_places is None or not isinstance(max_decimal_places, (int, float)):
max_decimal_places = get_global_setting('PRICING_DECIMAL_PLACES', 6)

value = Decimal(str(money.amount)).normalize()
value = str(value)

if decimal_places is not None:
if decimal_places is not None and isinstance(decimal_places, (int, float)):
# Decimal place count is provided, use it
pass
elif '.' in value:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,6 @@ def render_date(context, date_object):
return date_object


@register.simple_tag
def render_currency(money, **kwargs):
"""Render a currency / Money object."""
return InvenTree.helpers_model.render_currency(money, **kwargs)


@register.simple_tag()
def str2bool(x, *args, **kwargs):
"""Convert a string to a boolean value."""
Expand Down
61 changes: 44 additions & 17 deletions src/backend/InvenTree/part/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,48 @@
from stock.status_codes import StockStatus


class PartImageTestMixin:
"""Mixin for testing part images."""

roles = [
'part.change',
'part.add',
'part.delete',
'part_category.change',
'part_category.add',
]

@classmethod
def setUpTestData(cls):
"""Custom setup routine for this class."""
super().setUpTestData()

# Create a custom APIClient for file uploads
# Ref: https://stackoverflow.com/questions/40453947/how-to-generate-a-file-upload-test-request-with-django-rest-frameworks-apireq
cls.upload_client = APIClient()
cls.upload_client.force_authenticate(user=cls.user)

def create_test_image(self):
"""Create a test image file."""
p = Part.objects.first()

fn = BASE_DIR / '_testfolder' / 'part_image_123abc.png'

img = PIL.Image.new('RGB', (128, 128), color='blue')
img.save(fn)

with open(fn, 'rb') as img_file:
response = self.upload_client.patch(
reverse('api-part-detail', kwargs={'pk': p.pk}),
{'image': img_file},
expected_code=200,
)
print(response.data)
image_name = response.data['image']
self.assertTrue(image_name.startswith('/media/part_images/part_image'))
return image_name


class PartCategoryAPITest(InvenTreeAPITestCase):
"""Unit tests for the PartCategory API."""

Expand Down Expand Up @@ -1463,7 +1505,7 @@ def test_category_parameters(self):
self.assertEqual(prt.parameters.count(), 3)


class PartDetailTests(PartAPITestBase):
class PartDetailTests(PartImageTestMixin, PartAPITestBase):
"""Test that we can create / edit / delete Part objects via the API."""

@classmethod
Expand Down Expand Up @@ -1656,22 +1698,7 @@ def test_image_upload(self):
def test_existing_image(self):
"""Test that we can allocate an existing uploaded image to a new Part."""
# First, upload an image for an existing part
p = Part.objects.first()

fn = BASE_DIR / '_testfolder' / 'part_image_123abc.png'

img = PIL.Image.new('RGB', (128, 128), color='blue')
img.save(fn)

with open(fn, 'rb') as img_file:
response = self.upload_client.patch(
reverse('api-part-detail', kwargs={'pk': p.pk}),
{'image': img_file},
expected_code=200,
)

image_name = response.data['image']
self.assertTrue(image_name.startswith('/media/part_images/part_image'))
image_name = self.create_test_image()

# Attempt to create, but with an invalid image name
response = self.post(
Expand Down
56 changes: 2 additions & 54 deletions src/backend/InvenTree/report/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@
from django.urls import include, path
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _
from django.views.decorators.cache import cache_page, never_cache
from django.views.decorators.cache import never_cache

from django_filters import rest_framework as rest_filters
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import permissions
from rest_framework.generics import GenericAPIView
from rest_framework.request import clone_request
from rest_framework.response import Response

import common.models
Expand All @@ -25,12 +24,7 @@
from InvenTree.api import BulkDeleteMixin, MetadataView
from InvenTree.exceptions import log_error
from InvenTree.filters import InvenTreeSearchFilter
from InvenTree.mixins import (
ListAPI,
ListCreateAPI,
RetrieveAPI,
RetrieveUpdateDestroyAPI,
)
from InvenTree.mixins import ListAPI, ListCreateAPI, RetrieveUpdateDestroyAPI
from plugin.builtin.labels.inventree_label import InvenTreeLabelPlugin
from plugin.registry import registry

Expand All @@ -45,52 +39,6 @@ class TemplatePermissionMixin:
]


@method_decorator(cache_page(5), name='dispatch')
class TemplatePrintBase(RetrieveAPI):
"""Base class for printing against templates."""

@method_decorator(never_cache)
def dispatch(self, *args, **kwargs):
"""Prevent caching when printing report templates."""
return super().dispatch(*args, **kwargs)

def check_permissions(self, request):
"""Override request method to GET so that also non superusers can print using a post request."""
if request.method == 'POST':
request = clone_request(request, 'GET')
return super().check_permissions(request)

def post(self, request, *args, **kwargs):
"""Respond as if a POST request was provided."""
return self.get(request, *args, **kwargs)

def get(self, request, *args, **kwargs):
"""GET action for a template printing endpoint.

- Items are expected to be passed as a list of valid IDs
"""
# Extract a list of items to print from the queryset
item_ids = []

for value in request.query_params.get('items', '').split(','):
try:
item_ids.append(int(value))
except Exception:
pass

template = self.get_object()

items = template.get_model().objects.filter(pk__in=item_ids)

if len(items) == 0:
# At least one item must be provided
return Response(
{'error': _('No valid objects provided to template')}, status=400
)

return self.print(request, items)


class ReportFilterBase(rest_filters.FilterSet):
"""Base filter class for label and report templates."""

Expand Down
43 changes: 28 additions & 15 deletions src/backend/InvenTree/report/templatetags/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ def filter_queryset(queryset: QuerySet, **kwargs) -> QuerySet:
Example:
{% filter_queryset companies is_supplier=True as suppliers %}
"""
if not isinstance(queryset, QuerySet):
return queryset
return queryset.filter(**kwargs)


Expand All @@ -60,7 +62,10 @@ def filter_db_model(model_name: str, **kwargs) -> QuerySet:
Example:
{% filter_db_model 'part.partcategory' is_template=True as template_parts %}
"""
app_name, model_name = model_name.split('.')
try:
app_name, model_name = model_name.split('.')
except ValueError:
return None

try:
model = apps.get_model(app_name, model_name)
Expand Down Expand Up @@ -93,13 +98,7 @@ def getindex(container: list, index: int) -> Any:

if index < 0 or index >= len(container):
return None

try:
value = container[index]
except IndexError:
value = None

return value
return container[index]


@register.simple_tag()
Expand Down Expand Up @@ -191,8 +190,8 @@ def uploaded_image(
try:
full_path = settings.MEDIA_ROOT.joinpath(filename).resolve()
exists = full_path.exists() and full_path.is_file()
except Exception:
exists = False
except Exception: # pragma: no cover
exists = False # pragma: no cover

if exists and validate and not InvenTree.helpers.TestIfImage(full_path):
logger.warning("File '%s' is not a valid image", filename)
Expand Down Expand Up @@ -303,8 +302,12 @@ def part_image(part: Part, preview: bool = False, thumbnail: bool = False, **kwa
raise TypeError(_('part_image tag requires a Part instance'))

if preview:
if part.image and not hasattr(part.image, 'preview'):
return None
img = part.image.preview.name
elif thumbnail:
if part.image and not hasattr(part.image, 'thumbnail'):
return None
img = part.image.thumbnail.name
else:
img = part.image.name
Expand Down Expand Up @@ -376,10 +379,14 @@ def internal_link(link, text) -> str:
"""
text = str(text)

url = InvenTree.helpers_model.construct_absolute_url(link)
try:
url = InvenTree.helpers_model.construct_absolute_url(link)
except Exception:
url = None

# If the base URL is not set, just return the text
if not url:
logger.warning('Failed to construct absolute URL for internal link')
return text

return mark_safe(f'<a href="{url}">{text}</a>')
Expand Down Expand Up @@ -525,7 +532,10 @@ def format_date(dt: date, timezone: Optional[str] = None, fmt: Optional[str] = N
timezone: The timezone to use for the date (defaults to the server timezone)
fmt: The format string to use (defaults to ISO formatting)
"""
dt = InvenTree.helpers.to_local_time(dt, timezone).date()
try:
dt = InvenTree.helpers.to_local_time(dt, timezone).date()
except TypeError:
return str(dt)

if fmt:
return dt.strftime(fmt)
Expand Down Expand Up @@ -558,15 +568,18 @@ def icon(name, **kwargs):


@register.simple_tag()
def include_icon_fonts():
def include_icon_fonts(ttf: bool = False, woff: bool = False):
"""Return the CSS font-face rule for the icon fonts used on the current page (or all)."""
fonts = []

if not ttf and not woff:
ttf = woff = True

for font in common.icons.get_icon_packs().values():
# generate the font src string (prefer ttf over woff, woff2 is not supported by weasyprint)
if 'truetype' in font.fonts:
if 'truetype' in font.fonts and ttf:
font_format, url = 'truetype', font.fonts['truetype']
elif 'woff' in font.fonts:
elif 'woff' in font.fonts and woff:
font_format, url = 'woff', font.fonts['woff']

fonts.append(f"""
Expand Down
Loading
Loading