Skip to content
Merged
Show file tree
Hide file tree
Changes from 138 commits
Commits
Show all changes
153 commits
Select commit Hold shift + click to select a range
058e534
Add simple function for determining OPTIONS
SchrodingersGat Jun 23, 2021
eaa5913
Adds custom DRF metadata handler
SchrodingersGat Jun 23, 2021
82a6ff7
Adds unit testing for fancy new metadata class
SchrodingersGat Jun 23, 2021
b8a3117
Fix unit tests
SchrodingersGat Jun 23, 2021
2c1db2a
Further tweaks
SchrodingersGat Jun 23, 2021
0d9808f
Adds 'constructForm' javascript function
SchrodingersGat Jun 23, 2021
c387e1a
Working on functions to construct the various form components
SchrodingersGat Jun 23, 2021
aa02377
Updates for field rendering
SchrodingersGat Jun 23, 2021
66687a6
Now with error messages!
SchrodingersGat Jun 23, 2021
b71977b
Add field support
SchrodingersGat Jun 23, 2021
78232c2
Refactorin'
SchrodingersGat Jun 23, 2021
d77ca8a
Support URL fields
SchrodingersGat Jun 23, 2021
96ecd26
Support email fields
SchrodingersGat Jun 23, 2021
1a43198
actually run tests, for a change
SchrodingersGat Jun 23, 2021
6162129
Support choice field
SchrodingersGat Jun 23, 2021
4009ec8
Test fixes
SchrodingersGat Jun 23, 2021
e7bc53a
Working on a 'update' form
SchrodingersGat Jun 23, 2021
1754af3
Adds ability to specify which fields are displayed
SchrodingersGat Jun 23, 2021
9f3f07a
Refactor toot-toot
SchrodingersGat Jun 23, 2021
c8085ad
Skip nested objects
SchrodingersGat Jun 23, 2021
9feef93
Readonly fields
SchrodingersGat Jun 23, 2021
b350a97
Working on custom field info in metadata class
SchrodingersGat Jun 24, 2021
b273dc6
Scratch that
SchrodingersGat Jun 24, 2021
04374c7
Annotate models with their API list view
SchrodingersGat Jun 24, 2021
970a5d5
Include API endpoints in OPTIONS metadata
SchrodingersGat Jun 24, 2021
c5df91e
PEP Fix
SchrodingersGat Jun 24, 2021
9977b0b
Include model name in metadata
SchrodingersGat Jun 24, 2021
b20af54
Create select2 instance for related field
SchrodingersGat Jun 24, 2021
9e7d171
Fixes for select2 rendering issues
SchrodingersGat Jun 25, 2021
c07aedd
Move select2 files around
SchrodingersGat Jun 25, 2021
341467a
Fixes for base template
SchrodingersGat Jun 25, 2021
4cf69a5
Custom rendering functions
SchrodingersGat Jun 25, 2021
d411728
Start of custom rendering support based on model
SchrodingersGat Jun 25, 2021
b29db6f
Remove old debug message
SchrodingersGat Jun 25, 2021
565631e
More features
SchrodingersGat Jun 26, 2021
949c7dd
Set modal form title
SchrodingersGat Jun 26, 2021
e9db720
Extract field data on submit
SchrodingersGat Jun 26, 2021
9dd2765
Handle returned error messages
SchrodingersGat Jun 26, 2021
2eb7565
Callback handler for form success
SchrodingersGat Jun 26, 2021
f696bb2
Correctly read out boolean fields
SchrodingersGat Jun 26, 2021
67f76c8
Merge remote-tracking branch 'inventree/master' into drf-api-forms
SchrodingersGat Jun 26, 2021
d809483
Include 'default' value in OPTIONS request for any fields with specif…
SchrodingersGat Jun 27, 2021
0e9b82c
Load default values into rendered form
SchrodingersGat Jun 27, 2021
6335372
Store instance data when performing an "update"
SchrodingersGat Jun 27, 2021
ba2537d
Refactor the way that field options are passed to a form
SchrodingersGat Jun 28, 2021
e294223
Bug fix - check for null rather than just !
SchrodingersGat Jun 28, 2021
41539b7
Adds custom filters for AJAX queries
SchrodingersGat Jun 28, 2021
fbff9bf
Insert buttons for secondary modals
SchrodingersGat Jun 28, 2021
e585079
Callback function for fields after editing
SchrodingersGat Jun 28, 2021
f3ed05a
Automatically associate ''filters' with relations
SchrodingersGat Jun 28, 2021
ac7564d
Extract "limit_choices_to" options for relatedfields
SchrodingersGat Jun 28, 2021
ed2f21f
Display field prefix element in form
SchrodingersGat Jun 28, 2021
c3ef8d2
Fixes for model renderers
SchrodingersGat Jun 28, 2021
25a01be
Added warning message for missing model information
SchrodingersGat Jun 28, 2021
0037056
Better default renderer
SchrodingersGat Jun 28, 2021
374344d
Refactor switch statement
SchrodingersGat Jun 28, 2021
798bc17
Merge remote-tracking branch 'inventree/master' into drf-api-forms
SchrodingersGat Jun 28, 2021
9312a5d
Correctly render selected value of a related field
SchrodingersGat Jun 29, 2021
f18c2a7
Fix rendering during search
SchrodingersGat Jun 29, 2021
da6d170
Add 'help_text' for related fields
SchrodingersGat Jun 29, 2021
4aed699
Add some more unit tests
SchrodingersGat Jun 29, 2021
981cc2e
Fix select2 styling
SchrodingersGat Jun 29, 2021
5230a5a
Add "success" functionality for form posting
SchrodingersGat Jun 29, 2021
cf0feff
Allow override of values from calling function
SchrodingersGat Jun 29, 2021
c25967e
Replace CompanyCreate and CompanyEdit forms with AJAX form
SchrodingersGat Jun 29, 2021
170ed37
Delete CompanyCreate AJAX view
SchrodingersGat Jun 29, 2021
6156fff
Remove broken URLs
SchrodingersGat Jun 29, 2021
8b3a497
Remove unused Form
SchrodingersGat Jun 29, 2021
33ec91a
Add "default" from serializer field (if present)
SchrodingersGat Jun 29, 2021
293b5d4
Allow file and image fields
SchrodingersGat Jun 29, 2021
26eafe2
Replace PartImageUpload form
SchrodingersGat Jun 29, 2021
c425f36
Remove dead class
SchrodingersGat Jun 29, 2021
621f47e
Replace "edit part category" form
SchrodingersGat Jun 29, 2021
1f75530
Specify custom help text for fields on the client side
SchrodingersGat Jun 29, 2021
43f26f2
Allow custom labels
SchrodingersGat Jun 29, 2021
7d53bcb
Convert StockItemEditStatus to use API forms
SchrodingersGat Jun 29, 2021
87235b7
Replace StockItemAttachmentCreate form
SchrodingersGat Jun 29, 2021
54c9bd2
Add detail endpoint for StockItemAttachment
SchrodingersGat Jun 29, 2021
8c439e5
PEP fix
SchrodingersGat Jun 29, 2021
238dccc
Replace PartAttachmentCreate form
SchrodingersGat Jun 29, 2021
b946aed
Replace PartAttachmentEdit view
SchrodingersGat Jun 29, 2021
60d599b
Refactor PurchaseOrderAttachment views
SchrodingersGat Jun 30, 2021
712c959
Refactor SalesOrderAttachment forms
SchrodingersGat Jun 30, 2021
f67779c
Unit test fixes
SchrodingersGat Jun 30, 2021
a7d60cf
Exposes BuildOrderAttachment objects to the REST API
SchrodingersGat Jun 30, 2021
9ea3e51
Refactor BuildAttachment views
SchrodingersGat Jun 30, 2021
537c150
Fix for PK lookup in API test
SchrodingersGat Jun 30, 2021
653e3cd
Starting work on a DELETE form
SchrodingersGat Jun 30, 2021
4d8e88c
BuildAttachmentDelete form
SchrodingersGat Jun 30, 2021
4e23dbd
Refactor delete views for SalesOrderAttachment and PurchaseOrderAttac…
SchrodingersGat Jun 30, 2021
8f47035
Refactor delete view for PartAttachment and StockItemAttachment
SchrodingersGat Jun 30, 2021
09fff5b
Refactor PriceBreakCreate form
SchrodingersGat Jun 30, 2021
2b39417
Refactor update and delete forms for SupplierPriceBreak
SchrodingersGat Jun 30, 2021
a92fc7c
PEP fixes
SchrodingersGat Jun 30, 2021
682b2b4
Support rendering / updating of date inputs
SchrodingersGat Jun 30, 2021
9b4db43
Refactoring "attachment" tables to use the API
SchrodingersGat Jun 30, 2021
30ac5db
Display attachment upload date
SchrodingersGat Jun 30, 2021
770cd9a
Fix for LocationSerializer
SchrodingersGat Jun 30, 2021
5473174
Render simple choice fields with select2
SchrodingersGat Jun 30, 2021
59b794f
Cleanup old forms
SchrodingersGat Jul 1, 2021
bb0a72f
Refactor forms for StockItemTestResult
SchrodingersGat Jul 1, 2021
9d1c1b9
PEP fix
SchrodingersGat Jul 1, 2021
bfc5a7d
Refactor forms for PartTestTemplate model:
SchrodingersGat Jul 1, 2021
870542e
Refactor forms for ManufacturerPartParameter
SchrodingersGat Jul 1, 2021
96a2629
Remove old URL endpoints
SchrodingersGat Jul 1, 2021
206d7bd
Refactor edit and delete forms for ManufacturerPart
SchrodingersGat Jul 1, 2021
9bd71c1
Refactor deletion of multiple manufacturer part objects
SchrodingersGat Jul 1, 2021
8a29a3d
PEP fixes
SchrodingersGat Jul 1, 2021
225162a
Add ability to delete multiple selected manufacturer part parameters
SchrodingersGat Jul 1, 2021
e0f8310
Adds the ability to "clear" a non-required field with an obvious button
SchrodingersGat Jul 1, 2021
a771d77
Icon tweak
SchrodingersGat Jul 1, 2021
74d2334
Merge remote-tracking branch 'inventree/master' into drf-api-forms
SchrodingersGat Jul 1, 2021
2f1dea1
Modals can now be created programatically
SchrodingersGat Jul 2, 2021
3ff19f8
Refactorin'
SchrodingersGat Jul 2, 2021
00e921f
More work on dynamic modal template
SchrodingersGat Jul 2, 2021
51ebe30
Merge remote-tracking branch 'inventree/master' into drf-api-forms
SchrodingersGat Jul 2, 2021
9927b59
PEP fixes
SchrodingersGat Jul 2, 2021
52952a8
Handle different form responses
SchrodingersGat Jul 2, 2021
7252630
Renderer for supplier part
SchrodingersGat Jul 2, 2021
047b9d1
Capture enter key to submit form
SchrodingersGat Jul 2, 2021
291149d
New modal forms automatically work themselves out
SchrodingersGat Jul 2, 2021
2e3bfd1
Starting work on "secondary modal" functionality
SchrodingersGat Jul 2, 2021
366a2d5
Improve docstring
SchrodingersGat Jul 2, 2021
c7f834e
Display message when action is not allowed
SchrodingersGat Jul 2, 2021
746a021
Add option to focus on a particular field when launching
SchrodingersGat Jul 2, 2021
3756bd6
CSS tweaks
SchrodingersGat Jul 2, 2021
6d73265
Add support for custom placeholder text
SchrodingersGat Jul 2, 2021
2a4cbd6
Refactor setRelatedFieldData function
SchrodingersGat Jul 2, 2021
a47948f
Include model name in metadata
SchrodingersGat Jul 2, 2021
0791076
select2 fixes
SchrodingersGat Jul 2, 2021
298d870
Refactor CompanyDelete view
SchrodingersGat Jul 2, 2021
2ff9b23
Fixes for company serializer
SchrodingersGat Jul 2, 2021
5e9372f
Add API endpoints for Owner model
SchrodingersGat Jul 2, 2021
7e5c9aa
Refactor PurchaseOrderEdit form
SchrodingersGat Jul 2, 2021
984828f
Specify 'default' functions for 'reference' field in SalesOrder and P…
SchrodingersGat Jul 2, 2021
993abd9
Refactor forms for sales orders
SchrodingersGat Jul 2, 2021
1cdf03e
Added MinMoneyValidator to InvenTreeModelMoneyField
SchrodingersGat Jul 2, 2021
8c3a4b6
Refactoring forms for order line items
SchrodingersGat Jul 2, 2021
55f8fef
Remove old test
SchrodingersGat Jul 3, 2021
90a3a8a
Merge remote-tracking branch 'inventree/master' into drf-api-forms
SchrodingersGat Jul 3, 2021
77c3aa5
Use custom currency choices
SchrodingersGat Jul 3, 2021
359e92c
Merge remote-tracking branch 'inventree/master' into drf-api-forms
SchrodingersGat Jul 3, 2021
83256b1
Refactor BomItemDelete form
SchrodingersGat Jul 3, 2021
5c1fddd
JS bug fix
SchrodingersGat Jul 3, 2021
ce703bf
Adds detail API endpoint for StcokItemTracking model
SchrodingersGat Jul 3, 2021
8d1928f
Typo fix
SchrodingersGat Jul 3, 2021
2b7805e
PEP fix
SchrodingersGat Jul 3, 2021
8e280b6
Increment API version
SchrodingersGat Jul 3, 2021
25a80d2
Fix form filters
SchrodingersGat Jul 3, 2021
c524f75
Allow null values for purchase_price and sale_price
SchrodingersGat Jul 3, 2021
889834b
Refactor POLineItemCreate form
SchrodingersGat Jul 3, 2021
699b21f
Remove broken URL
SchrodingersGat Jul 3, 2021
3cc9299
Refactor SOLineItemCreate view
SchrodingersGat Jul 3, 2021
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
16 changes: 16 additions & 0 deletions InvenTree/InvenTree/api_tester.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,22 @@ def assignRole(self, role):
ruleset.save()
break

def getActions(self, url):
"""
Return a dict of the 'actions' available at a given endpoint.
Makes use of the HTTP 'OPTIONS' method to request this.
"""

response = self.client.options(url)
self.assertEqual(response.status_code, 200)

actions = response.data.get('actions', None)

if not actions:
actions = {}

return actions

def get(self, url, data={}, expected_code=200):
"""
Issue a GET request
Expand Down
17 changes: 16 additions & 1 deletion InvenTree/InvenTree/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
from django import forms

from decimal import Decimal

from djmoney.models.fields import MoneyField as ModelMoneyField
from djmoney.forms.fields import MoneyField
from djmoney.models.validators import MinMoneyValidator

import InvenTree.helpers
import common.settings
Expand Down Expand Up @@ -47,7 +49,10 @@ def money_kwargs():


class InvenTreeModelMoneyField(ModelMoneyField):
""" custom MoneyField for clean migrations while using dynamic currency settings """
"""
Custom MoneyField for clean migrations while using dynamic currency settings
"""

def __init__(self, **kwargs):
# detect if creating migration
if 'makemigrations' in sys.argv:
Expand All @@ -58,6 +63,16 @@ def __init__(self, **kwargs):
# set defaults
kwargs.update(money_kwargs())

# Set a minimum value validator
validators = kwargs.get('validators', [])

if len(validators) == 0:
validators.append(
MinMoneyValidator(0),
)

kwargs['validators'] = validators

super().__init__(**kwargs)

def formfield(self, **kwargs):
Expand Down
172 changes: 172 additions & 0 deletions InvenTree/InvenTree/metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# -*- coding: utf-8 -*-

from __future__ import unicode_literals

import logging

from rest_framework import serializers
from rest_framework.metadata import SimpleMetadata
from rest_framework.utils import model_meta
from rest_framework.fields import empty

import users.models


logger = logging.getLogger('inventree')


class InvenTreeMetadata(SimpleMetadata):
"""
Custom metadata class for the DRF API.

This custom metadata class imits the available "actions",
based on the user's role permissions.

Thus when a client send an OPTIONS request to an API endpoint,
it will only receive a list of actions which it is allowed to perform!

Additionally, we include some extra information about database models,
so we can perform lookup for ForeignKey related fields.

"""

def determine_metadata(self, request, view):

metadata = super().determine_metadata(request, view)

user = request.user

if user is None:
# No actions for you!
metadata['actions'] = {}
return metadata

try:
# Extract the model name associated with the view
self.model = view.serializer_class.Meta.model

# Construct the 'table name' from the model
app_label = self.model._meta.app_label
tbl_label = self.model._meta.model_name

metadata['model'] = tbl_label

table = f"{app_label}_{tbl_label}"

actions = metadata.get('actions', None)

if actions is not None:

check = users.models.RuleSet.check_table_permission

# Map the request method to a permission type
rolemap = {
'POST': 'add',
'PUT': 'change',
'PATCH': 'change',
'DELETE': 'delete',
}

# Remove any HTTP methods that the user does not have permission for
for method, permission in rolemap.items():
if method in actions and not check(user, table, permission):
del actions[method]

# Add a 'DELETE' action if we are allowed to delete
if 'DELETE' in view.allowed_methods and check(user, table, 'delete'):
actions['DELETE'] = True

# Add a 'VIEW' action if we are allowed to view
if 'GET' in view.allowed_methods and check(user, table, 'view'):
actions['GET'] = True

except AttributeError:
# We will assume that if the serializer class does *not* have a Meta
# then we don't need a permission
pass

return metadata

def get_serializer_info(self, serializer):
"""
Override get_serializer_info so that we can add 'default' values
to any fields whose Meta.model specifies a default value
"""

serializer_info = super().get_serializer_info(serializer)

try:
ModelClass = serializer.Meta.model

model_fields = model_meta.get_field_info(ModelClass)

# Iterate through simple fields
for name, field in model_fields.fields.items():

if field.has_default() and name in serializer_info.keys():

default = field.default

if callable(default):
try:
default = default()
except:
continue

serializer_info[name]['default'] = default

# Iterate through relations
for name, relation in model_fields.relations.items():

if name not in serializer_info.keys():
# Skip relation not defined in serializer
continue

if relation.reverse:
# Ignore reverse relations
continue

# Extract and provide the "limit_choices_to" filters
# This is used to automatically filter AJAX requests
serializer_info[name]['filters'] = relation.model_field.get_limit_choices_to()

if 'help_text' not in serializer_info[name] and hasattr(relation.model_field, 'help_text'):
serializer_info[name]['help_text'] = relation.model_field.help_text

except AttributeError:
pass

return serializer_info

def get_field_info(self, field):
"""
Given an instance of a serializer field, return a dictionary
of metadata about it.

We take the regular DRF metadata and add our own unique flavor
"""

# Run super method first
field_info = super().get_field_info(field)

# If a default value is specified for the serializer field, add it!
if 'default' not in field_info and not field.default == empty:
field_info['default'] = field.get_default()

# Introspect writable related fields
if field_info['type'] == 'field' and not field_info['read_only']:

# If the field is a PrimaryKeyRelatedField, we can extract the model from the queryset
if isinstance(field, serializers.PrimaryKeyRelatedField):
model = field.queryset.model
else:
logger.debug("Could not extract model for:", field_info['label'], '->', field)
model = None

if model:
# Mark this field as "related", and point to the URL where we can get the data!
field_info['type'] = 'related field'
field_info['api_url'] = model.get_api_url()
field_info['model'] = model._meta.model_name

return field_info
11 changes: 11 additions & 0 deletions InvenTree/InvenTree/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError

from django.db.models.signals import pre_delete
from django.dispatch import receiver

from mptt.models import MPTTModel, TreeForeignKey
from mptt.exceptions import InvalidMove

from .validators import validate_tree_name

Expand Down Expand Up @@ -91,6 +93,15 @@ class InvenTreeTree(MPTTModel):
parent: The item immediately above this one. An item with a null parent is a top-level item
"""

def save(self, *args, **kwargs):

try:
super().save(*args, **kwargs)
except InvalidMove:
raise ValidationError({
'parent': _("Invalid choice"),
})

class Meta:
abstract = True

Expand Down
55 changes: 55 additions & 0 deletions InvenTree/InvenTree/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,58 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals


import os

from decimal import Decimal

from django.conf import settings
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError as DjangoValidationError
from django.utils.translation import ugettext_lazy as _

from djmoney.contrib.django_rest_framework.fields import MoneyField
from djmoney.money import Money
from djmoney.utils import MONEY_CLASSES, get_currency_field_name

from rest_framework import serializers
from rest_framework.utils import model_meta
from rest_framework.fields import empty
from rest_framework.exceptions import ValidationError
from rest_framework.serializers import DecimalField


class InvenTreeMoneySerializer(MoneyField):
"""
Custom serializer for 'MoneyField',
which ensures that passed values are numerically valid

Ref: https://github.com/django-money/django-money/blob/master/djmoney/contrib/django_rest_framework/fields.py
"""

def get_value(self, data):
"""
Test that the returned amount is a valid Decimal
"""

amount = super(DecimalField, self).get_value(data)

# Convert an empty string to None
if len(str(amount).strip()) == 0:
amount = None

try:
if amount is not None:
amount = Decimal(amount)
except:
raise ValidationError(_("Must be a valid number"))

currency = data.get(get_currency_field_name(self.field_name), self.default_currency)

if currency and amount is not None and not isinstance(amount, MONEY_CLASSES) and amount is not empty:
return Money(amount, currency)

return amount


class UserSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -110,6 +152,19 @@ def get_initial(self):

return initials

def save(self, **kwargs):
"""
Catch any django ValidationError thrown at the moment save() is called,
and re-throw as a DRF ValidationError
"""

try:
super().save(**kwargs)
except (ValidationError, DjangoValidationError) as exc:
raise ValidationError(detail=serializers.as_serializer_error(exc))

return self.instance

def run_validation(self, data=empty):
"""
Perform serializer validation.
Expand Down
1 change: 1 addition & 0 deletions InvenTree/InvenTree/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ def get_setting(environment_var, backup_val, default_value=None):
'InvenTree.permissions.RolePermission',
),
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
'DEFAULT_METADATA_CLASS': 'InvenTree.metadata.InvenTreeMetadata'
}

WSGI_APPLICATION = 'InvenTree.wsgi.application'
Expand Down
Loading