Skip to content

Commit

Permalink
Merge branch 'master' into add-selectionList
Browse files Browse the repository at this point in the history
  • Loading branch information
matmair authored Nov 27, 2024
2 parents b6ad1a9 + 20fb125 commit 58e321f
Show file tree
Hide file tree
Showing 10 changed files with 159 additions and 122 deletions.
72 changes: 72 additions & 0 deletions docs/docs/report/helpers.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,78 @@ To return an element corresponding to a certain key in a container which support
{% endraw %}
```

## Database Helpers

A number of helper functions are available for accessing database objects:

### filter_queryset

The `filter_queryset` function allows for arbitrary filtering of the provided querysert. It takes a queryset and a list of filter arguments, and returns a filtered queryset.



::: report.templatetags.report.filter_queryset
options:
show_docstring_description: false
show_source: False

!!! info "Provided QuerySet"
The provided queryset must be a valid Django queryset object, which is already available in the template context.

!!! warning "Advanced Users"
The `filter_queryset` function is a powerful tool, but it is also easy to misuse. It assumes that the user has a good understanding of Django querysets and the underlying database structure.

#### Example

In a report template which has a `PurchaseOrder` object available in its context, fetch any line items which have a received quantity greater than zero:

```html
{% raw %}
{% load report %}

{% filter_queryset order.lines.all received__gt=0 as received_lines %}

<ul>
{% for line in received_lines %}
<li>{{ line.part.part.full_name }} - {{ line.received }} / {{ line.quantity }}</li>
{% endfor %}
</ul>

{% endraw %}
```

### filter_db_model

The `filter_db_model` function allows for filtering of a database model based on a set of filter arguments. It takes a model class and a list of filter arguments, and returns a filtered queryset.

::: report.templatetags.report.filter_db_model
options:
show_docstring_description: false
show_source: False

#### Example

Generate a list of all active customers:

```html
{% raw %}
{% load report %}

{% filter_db_model company.company is_customer=True active=True as active_customers %}

<ul>
{% for customer in active_customers %}
<li>{{ customer.name }}</li>
{% endfor %}
</ul>

{% endraw %}
```

### Advanced Database Queries

More advanced database filtering should be achieved using a [report plugin](../extend/plugins/report.md), and adding custom context data to the report template.

## Number Formatting

### format_number
Expand Down
39 changes: 17 additions & 22 deletions src/backend/InvenTree/InvenTree/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,8 @@

import bleach
import pytz
import regex
from bleach import clean
from djmoney.money import Money
from PIL import Image

from common.currency import currency_code_default

Expand Down Expand Up @@ -140,6 +138,8 @@ def getStaticUrl(filename):

def TestIfImage(img):
"""Test if an image file is indeed an image."""
from PIL import Image

try:
Image.open(img).verify()
return True
Expand Down Expand Up @@ -781,6 +781,8 @@ def strip_html_tags(value: str, raise_error=True, field_name=None):
If raise_error is True, a ValidationError will be thrown if HTML tags are detected
"""
value = str(value).strip()

cleaned = clean(value, strip=True, tags=[], attributes=[])

# Add escaped characters back in
Expand All @@ -792,39 +794,32 @@ def strip_html_tags(value: str, raise_error=True, field_name=None):
# If the length changed, it means that HTML tags were removed!
if len(cleaned) != len(value) and raise_error:
field = field_name or 'non_field_errors'

raise ValidationError({field: [_('Remove HTML tags from this value')]})

return cleaned


def remove_non_printable_characters(
value: str, remove_newline=True, remove_ascii=True, remove_unicode=True
):
def remove_non_printable_characters(value: str, remove_newline=True) -> str:
"""Remove non-printable / control characters from the provided string."""
cleaned = value

if remove_ascii:
# Remove ASCII control characters
# Note that we do not sub out 0x0A (\n) here, it is done separately below
cleaned = regex.sub('[\x00-\x09]+', '', cleaned)
cleaned = regex.sub('[\x0b-\x1f\x7f]+', '', cleaned)
# Remove ASCII control characters
# Note that we do not sub out 0x0A (\n) here, it is done separately below
regex = re.compile(r'[\u0000-\u0009\u000B-\u001F\u007F-\u009F]')
cleaned = regex.sub('', cleaned)

if remove_newline:
cleaned = regex.sub('[\x0a]+', '', cleaned)
# Remove Unicode control characters
regex = re.compile(r'[\u200E\u200F\u202A-\u202E]')
cleaned = regex.sub('', cleaned)

if remove_unicode:
# Remove Unicode control characters
if remove_newline:
cleaned = regex.sub(r'[^\P{C}]+', '', cleaned)
else:
# Use 'negative-lookahead' to exclude newline character
cleaned = regex.sub('(?![\x0a])[^\\P{C}]+', '', cleaned)
if remove_newline:
regex = re.compile(r'[\x0A]')
cleaned = regex.sub('', cleaned)

return cleaned


def clean_markdown(value: str):
def clean_markdown(value: str) -> str:
"""Clean a markdown string.
This function will remove javascript and other potentially harmful content from the markdown string.
Expand Down Expand Up @@ -883,7 +878,7 @@ def clean_markdown(value: str):
return value


def hash_barcode(barcode_data):
def hash_barcode(barcode_data: str) -> str:
"""Calculate a 'unique' hash for a barcode string.
This hash is used for comparison / lookup.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Custom management command to prerender files."""

import logging
import os

from django.conf import settings
Expand All @@ -9,6 +10,8 @@
from django.utils.module_loading import import_string
from django.utils.translation import override as lang_over

logger = logging.getLogger('inventree')


def render_file(file_name, source, target, locales, ctx):
"""Renders a file into all provided locales."""
Expand All @@ -31,6 +34,10 @@ class Command(BaseCommand):

def handle(self, *args, **kwargs):
"""Django command to prerender files."""
if not settings.ENABLE_CLASSIC_FRONTEND:
logger.info('Classic frontend is disabled. Skipping prerendering.')
return

# static directories
LC_DIR = settings.LOCALE_PATHS[0]
SOURCE_DIR = settings.STATICFILES_I18_SRC
Expand Down
8 changes: 5 additions & 3 deletions src/backend/InvenTree/InvenTree/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def clean_string(self, field: str, data: str) -> str:
Ref: https://github.com/mozilla/bleach/issues/192
"""
cleaned = strip_html_tags(data, field_name=field)
cleaned = data

# By default, newline characters are removed
remove_newline = True
Expand All @@ -66,13 +66,13 @@ def clean_string(self, field: str, data: str) -> str:
try:
if hasattr(self, 'serializer_class'):
model = self.serializer_class.Meta.model
field = model._meta.get_field(field)
field_base = model._meta.get_field(field)

# The following field types allow newline characters
allow_newline = [(InvenTreeNotesField, True)]

for field_type in allow_newline:
if issubclass(type(field), field_type[0]):
if issubclass(type(field_base), field_type[0]):
remove_newline = False
is_markdown = field_type[1]
break
Expand All @@ -86,6 +86,8 @@ def clean_string(self, field: str, data: str) -> str:
cleaned, remove_newline=remove_newline
)

cleaned = strip_html_tags(cleaned, field_name=field)

if is_markdown:
cleaned = clean_markdown(cleaned)

Expand Down
2 changes: 2 additions & 0 deletions src/backend/InvenTree/build/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -871,6 +871,7 @@ def create_build_output(self, quantity, **kwargs):
part=self.part,
build=self,
batch=batch,
location=location,
is_building=True
)

Expand Down Expand Up @@ -1749,6 +1750,7 @@ def complete_allocation(self, user, notes=''):
else:
# Mark the item as "consumed" by the build order
item.consumed_by = self.build
item.location = None
item.save(add_note=False)

item.add_tracking_entry(
Expand Down
9 changes: 9 additions & 0 deletions src/backend/InvenTree/part/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,15 @@ def test_invisible_chars(self):
'A\t part\t category\t',
'A pa\rrt cat\r\r\regory',
'A part\u200e catego\u200fry\u202e',
'A\u0000 part\u0000 category',
'A part\u0007 category',
'A\u001f part category',
'A part\u007f category',
'\u0001A part category',
'A part\u0085 category',
'A part category\u200e',
'A part cat\u200fegory',
'A\u0006 part\u007f categ\nory\r',
]

for val in values:
Expand Down
46 changes: 46 additions & 0 deletions src/backend/InvenTree/report/templatetags/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
from typing import Any, Optional

from django import template
from django.apps.registry import apps
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db.models.query import QuerySet
from django.utils.safestring import SafeString, mark_safe
from django.utils.translation import gettext_lazy as _

Expand All @@ -29,6 +31,50 @@
logger = logging.getLogger('inventree')


@register.simple_tag()
def filter_queryset(queryset: QuerySet, **kwargs) -> QuerySet:
"""Filter a database queryset based on the provided keyword arguments.
Arguments:
queryset: The queryset to filter
Keyword Arguments:
field (any): Filter the queryset based on the provided field
Example:
{% filter_queryset companies is_supplier=True as suppliers %}
"""
return queryset.filter(**kwargs)


@register.simple_tag()
def filter_db_model(model_name: str, **kwargs) -> QuerySet:
"""Filter a database model based on the provided keyword arguments.
Arguments:
model_name: The name of the Django model - including app name (e.g. 'part.partcategory')
Keyword Arguments:
field (any): Filter the queryset based on the provided field
Example:
{% filter_db_model 'part.partcategory' is_template=True as template_parts %}
"""
app_name, model_name = model_name.split('.')

try:
model = apps.get_model(app_name, model_name)
except Exception:
return None

if model is None:
return None

queryset = model.objects.all()

return filter_queryset(queryset, **kwargs)


@register.simple_tag()
def getindex(container: list, index: int) -> Any:
"""Return the value contained at the specified index of the list.
Expand Down
1 change: 1 addition & 0 deletions src/backend/InvenTree/stock/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1403,6 +1403,7 @@ def installStockItem(self, other_item, quantity, user, notes, build=None):
# Assign the other stock item into this one
stock_item.belongs_to = self
stock_item.consumed_by = build
stock_item.location = None
stock_item.save(add_note=False)

deltas = {'stockitem': self.pk}
Expand Down
1 change: 0 additions & 1 deletion src/backend/requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ python-dotenv # Environment variable management
pyyaml>=6.0.1 # YAML parsing
qrcode[pil] # QR code generator
rapidfuzz # Fuzzy string matching
regex # Advanced regular expressions
sentry-sdk # Error reporting (optional)
setuptools # Standard dependency
tablib[xls,xlsx,yaml] # Support for XLS and XLSX formats
Expand Down
Loading

0 comments on commit 58e321f

Please sign in to comment.