Skip to content

Commit

Permalink
Merge branch 'master' into plugin-settings-ui
Browse files Browse the repository at this point in the history
  • Loading branch information
SchrodingersGat authored Oct 7, 2024
2 parents 8c5131b + 846b17a commit 3b67840
Show file tree
Hide file tree
Showing 22 changed files with 328 additions and 40 deletions.
32 changes: 32 additions & 0 deletions docs/docs/demo.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,35 @@ The demo instance has a number of user accounts which you can use to explore the
| ian | inactive | No | No | Inactive account, cannot log in |
| susan | inactive | No | No | Inactive account, cannot log in |
| admin | inventree | Yes | Yes | Superuser account, can access all parts of the system |

### Dataset

The demo instance is populated with a sample dataset, which is reset every 24 hours.

The source data used in the demo instance can be found on our [GitHub page](https://github.com/inventree/demo-dataset).

### Local Setup

If you wish to install the demo dataset locally (for initial testing), you can run the following command:

```bash
invoke dev.setup-test -i
```

*(Note: The command above may be slightly different if you are running in docker.)*

This will install the demo dataset into your local InvenTree instance.

!!! warning "Warning"
This command will **delete all existing data** in your InvenTree instance! It is not intended to be used on a production system, or loaded into an existing dataset.

### Clear Data

To clear demo data from your instance, and start afresh with a clean database, you can run the following command:

```bash
invoke dev.delete-data
```

!!! warning "Warning"
This command will **delete all existing data** in your InvenTree instance, including any data that you have added yourself.
2 changes: 1 addition & 1 deletion docs/docs/develop/devcontainer.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Tasks can help you executing scripts. You can run them by open the command panel
#### Setup demo dataset
If you need some demo test-data, run the `setup-test` task. This will import an `admin` user with the password `inventree`. For more info on what this dataset contains see [inventree/demo-dataset](https://github.com/inventree/demo-dataset).
If you need some demo test-data, run the `setup-test` task. This will import an `admin` user with the password `inventree`. For more info on what this dataset contains see [inventree/demo-dataset](../demo.md).
#### Setup a superuser
Expand Down
34 changes: 34 additions & 0 deletions src/backend/InvenTree/InvenTree/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,40 @@ def remove_non_printable_characters(
return cleaned


def clean_markdown(value: str):
"""Clean a markdown string.
This function will remove javascript and other potentially harmful content from the markdown string.
"""
import markdown
from markdownify.templatetags.markdownify import markdownify

try:
markdownify_settings = settings.MARKDOWNIFY['default']
except (AttributeError, KeyError):
markdownify_settings = {}

extensions = markdownify_settings.get('MARKDOWN_EXTENSIONS', [])
extension_configs = markdownify_settings.get('MARKDOWN_EXTENSION_CONFIGS', {})

# Generate raw HTML from provided markdown (without sanitizing)
# Note: The 'html' output_format is required to generate self closing tags, e.g. <tag> instead of <tag />
html = markdown.markdown(
value or '',
extensions=extensions,
extension_configs=extension_configs,
output_format='html',
)

# Clean the HTML content (for comparison). Ideally, this should be the same as the original content
clean_html = markdownify(value)

if html != clean_html:
raise ValidationError(_('Data contains prohibited markdown content'))

return value


def hash_barcode(barcode_data):
"""Calculate a 'unique' hash for a barcode string.
Expand Down
15 changes: 12 additions & 3 deletions src/backend/InvenTree/InvenTree/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
from rest_framework.response import Response

from InvenTree.fields import InvenTreeNotesField
from InvenTree.helpers import remove_non_printable_characters, strip_html_tags
from InvenTree.helpers import (
clean_markdown,
remove_non_printable_characters,
strip_html_tags,
)


class CleanMixin:
Expand Down Expand Up @@ -57,18 +61,20 @@ def clean_string(self, field: str, data: str) -> str:

# By default, newline characters are removed
remove_newline = True
is_markdown = False

try:
if hasattr(self, 'serializer_class'):
model = self.serializer_class.Meta.model
field = model._meta.get_field(field)

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

for field_type in allow_newline:
if issubclass(type(field), field_type):
if issubclass(type(field), field_type[0]):
remove_newline = False
is_markdown = field_type[1]
break

except AttributeError:
Expand All @@ -80,6 +86,9 @@ def clean_string(self, field: str, data: str) -> str:
cleaned, remove_newline=remove_newline
)

if is_markdown:
cleaned = clean_markdown(cleaned)

return cleaned

def clean_data(self, data: dict) -> dict:
Expand Down
7 changes: 6 additions & 1 deletion src/backend/InvenTree/InvenTree/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -1264,23 +1264,28 @@
'abbr',
'b',
'blockquote',
'code',
'em',
'h1',
'h2',
'h3',
'h4',
'h5',
'hr',
'i',
'img',
'li',
'ol',
'p',
's',
'strong',
'ul',
'table',
'thead',
'tbody',
'th',
'tr',
'td',
'ul',
],
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/backend/InvenTree/InvenTree/static/script/purify.min.js

Large diffs are not rendered by default.

13 changes: 10 additions & 3 deletions src/backend/InvenTree/build/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1606,12 +1606,19 @@ def clean(self):
'quantity': _(f'Allocated quantity ({q}) must not exceed available stock quantity ({a})')
})

# Allocated quantity cannot cause the stock item to be over-allocated
# Ensure that we do not 'over allocate' a stock item
available = decimal.Decimal(self.stock_item.quantity)
allocated = decimal.Decimal(self.stock_item.allocation_count())
quantity = decimal.Decimal(self.quantity)
build_allocation_count = decimal.Decimal(self.stock_item.build_allocation_count(
exclude_allocations={'pk': self.pk}
))
sales_allocation_count = decimal.Decimal(self.stock_item.sales_order_allocation_count())

if available - allocated + quantity < quantity:
total_allocation = (
build_allocation_count + sales_allocation_count + quantity
)

if total_allocation > available:
raise ValidationError({
'quantity': _('Stock item is over-allocated')
})
Expand Down
59 changes: 59 additions & 0 deletions src/backend/InvenTree/build/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -993,6 +993,65 @@ def test_fractional_allocation(self):
expected_code=201,
)

class BuildItemTest(BuildAPITest):
"""Unit tests for build items.
For this test, we will be using Build ID=1;
- This points to Part 100 (see fixture data in part.yaml)
- This Part already has a BOM with 4 items (see fixture data in bom.yaml)
- There are no BomItem objects yet created for this build
"""

def setUp(self):
"""Basic operation as part of test suite setup"""
super().setUp()

self.assignRole('build.add')
self.assignRole('build.change')

self.build = Build.objects.get(pk=1)

# Regenerate BuildLine objects
self.build.create_build_line_items()

# Record number of build items which exist at the start of each test
self.n = BuildItem.objects.count()

def test_update_overallocated(self):
"""Test update of overallocated stock items."""

si = StockItem.objects.get(pk=2)

# Find line item
line = self.build.build_lines.all().filter(bom_item__sub_part=si.part).first()

# Set initial stock item quantity
si.quantity = 100
si.save()

# Create build item
bi = BuildItem(
build_line=line,
stock_item=si,
quantity=100
)
bi.save()

# Reduce stock item quantity
si.quantity = 50
si.save()

# Reduce build item quantity
url = reverse('api-build-item-detail', kwargs={'pk': bi.pk})

self.patch(
url,
{
"quantity": 50,
},
expected_code=200,
)

class BuildOverallocationTest(BuildAPITest):
"""Unit tests for over allocation of stock items against a build order.
Expand Down
41 changes: 41 additions & 0 deletions src/backend/InvenTree/company/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,47 @@ def test_company_active(self):
len(self.get(url, data={'active': False}, expected_code=200).data), 1
)

def test_company_notes(self):
"""Test the markdown 'notes' field for the Company model."""
pk = Company.objects.first().pk

# Attempt to inject malicious markdown into the "notes" field
xss = [
'[Click me](javascript:alert(123))',
'![x](javascript:alert(123))',
'![Uh oh...]("onerror="alert(\'XSS\'))',
]

for note in xss:
response = self.patch(
reverse('api-company-detail', kwargs={'pk': pk}),
{'notes': note},
expected_code=400,
)

self.assertIn(
'Data contains prohibited markdown content', str(response.data)
)

# The following markdown is safe, and should be accepted
good = [
'This is a **bold** statement',
'This is a *italic* statement',
'This is a [link](https://www.google.com)',
'This is an ![image](https://www.google.com/test.jpg)',
'This is a `code` block',
'This text has ~~strikethrough~~ formatting',
]

for note in good:
response = self.patch(
reverse('api-company-detail', kwargs={'pk': pk}),
{'notes': note},
expected_code=200,
)

self.assertEqual(response.data['notes'], note)


class ContactTest(InvenTreeAPITestCase):
"""Tests for the Contact models."""
Expand Down
14 changes: 11 additions & 3 deletions src/backend/InvenTree/stock/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1190,9 +1190,17 @@ def is_allocated(self):

return self.sales_order_allocations.count() > 0

def build_allocation_count(self):
"""Return the total quantity allocated to builds."""
query = self.allocations.aggregate(q=Coalesce(Sum('quantity'), Decimal(0)))
def build_allocation_count(self, **kwargs):
"""Return the total quantity allocated to builds, with optional filters."""
query = self.allocations.all()

if filter_allocations := kwargs.get('filter_allocations'):
query = query.filter(**filter_allocations)

if exclude_allocations := kwargs.get('exclude_allocations'):
query = query.exclude(**exclude_allocations)

query = query.aggregate(q=Coalesce(Sum('quantity'), Decimal(0)))

total = query['q']

Expand Down
5 changes: 5 additions & 0 deletions src/backend/InvenTree/templates/js/translated/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,11 @@ function setupNotesField(element, url, options={}) {
}
});
},
renderingConfig: {
sanitizerFunction: function (html) {
return DOMPurify.sanitize(html);
}
},
shortcuts: [],
});

Expand Down
1 change: 1 addition & 0 deletions src/backend/InvenTree/templates/third_party_js.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@
<script defer type='text/javascript' src="{% static 'script/randomColor.min.js' %}"></script>
<script defer type='text/javascript' src="{% static 'script/html5-qrcode.min.js' %}"></script>
<script defer type='text/javascript' src="{% static 'script/qrcode.min.js' %}"></script>
<script defer type='text/javascript' src="{% static 'script/purify.min.js' %}"></script>
1 change: 1 addition & 0 deletions src/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"clsx": "^2.1.1",
"codemirror": "^6.0.1",
"dayjs": "^1.11.13",
"dompurify": "^3.1.7",
"easymde": "^2.18.0",
"embla-carousel-react": "^8.3.0",
"fuse.js": "^7.0.0",
Expand Down
Loading

0 comments on commit 3b67840

Please sign in to comment.