mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Refactor model helpers into own file (#4927)
* Refactor model helpers into own file to allow helper import when apps not loaded yet * Import helper functions at module level * Added missing imports where vscode couldnt help because its no explicit import
This commit is contained in:
parent
a196f443a1
commit
99d122baa9
@ -12,11 +12,14 @@ from djmoney.models.fields import MoneyField as ModelMoneyField
|
||||
from djmoney.models.validators import MinMoneyValidator
|
||||
from rest_framework.fields import URLField as RestURLField
|
||||
|
||||
import InvenTree.helpers
|
||||
|
||||
from .validators import AllowedURLValidator, allowable_url_schemes
|
||||
|
||||
|
||||
class InvenTreeRestURLField(RestURLField):
|
||||
"""Custom field for DRF with custom scheme vaildators."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Update schemes."""
|
||||
|
||||
@ -109,6 +112,7 @@ class InvenTreeModelMoneyField(ModelMoneyField):
|
||||
|
||||
class InvenTreeMoneyField(MoneyField):
|
||||
"""Custom MoneyField for clean migrations while using dynamic currency settings."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Override initial values with the real info from database."""
|
||||
kwargs.update(money_kwargs())
|
||||
@ -148,8 +152,6 @@ class DatePickerFormField(forms.DateField):
|
||||
def round_decimal(value, places, normalize=False):
|
||||
"""Round value to the specified number of places."""
|
||||
|
||||
import InvenTree.helpers
|
||||
|
||||
if type(value) in [Decimal, float]:
|
||||
value = round(value, places)
|
||||
|
||||
|
@ -14,23 +14,15 @@ from django.conf import settings
|
||||
from django.contrib.staticfiles.storage import StaticFilesStorage
|
||||
from django.core.exceptions import FieldError, ValidationError
|
||||
from django.core.files.storage import default_storage
|
||||
from django.core.validators import URLValidator
|
||||
from django.db.utils import OperationalError, ProgrammingError
|
||||
from django.http import StreamingHttpResponse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import moneyed.localization
|
||||
import regex
|
||||
import requests
|
||||
from bleach import clean
|
||||
from djmoney.contrib.exchange.models import convert_money
|
||||
from djmoney.money import Money
|
||||
from PIL import Image
|
||||
|
||||
import common.models
|
||||
import InvenTree.version
|
||||
from common.notifications import (InvenTreeNotificationBodies,
|
||||
NotificationBody, trigger_notification)
|
||||
from common.settings import currency_code_default
|
||||
|
||||
from .settings import MEDIA_URL, STATIC_URL
|
||||
@ -38,11 +30,6 @@ from .settings import MEDIA_URL, STATIC_URL
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
def getSetting(key, backup_value=None):
|
||||
"""Shortcut for reading a setting value from the database."""
|
||||
return common.models.InvenTreeSetting.get_setting(key, backup_value=backup_value)
|
||||
|
||||
|
||||
def generateTestKey(test_name):
|
||||
"""Generate a test 'key' for a given test name. This must not have illegal chars as it will be used for dict lookup in a template.
|
||||
|
||||
@ -85,156 +72,6 @@ def getStaticUrl(filename):
|
||||
return os.path.join(STATIC_URL, str(filename))
|
||||
|
||||
|
||||
def construct_absolute_url(*arg, **kwargs):
|
||||
"""Construct (or attempt to construct) an absolute URL from a relative URL.
|
||||
|
||||
This is useful when (for example) sending an email to a user with a link
|
||||
to something in the InvenTree web framework.
|
||||
|
||||
A URL is constructed in the following order:
|
||||
|
||||
1. If setings.SITE_URL is set (e.g. in the Django settings), use that
|
||||
2. If the InvenTree setting INVENTREE_BASE_URL is set, use that
|
||||
3. Otherwise, use the current request URL (if available)
|
||||
"""
|
||||
|
||||
relative_url = '/'.join(arg)
|
||||
|
||||
# If a site URL is provided, use that
|
||||
site_url = getattr(settings, 'SITE_URL', None)
|
||||
|
||||
if not site_url:
|
||||
# Otherwise, try to use the InvenTree setting
|
||||
try:
|
||||
site_url = common.models.InvenTreeSetting.get_setting('INVENTREE_BASE_URL', create=False, cache=False)
|
||||
except ProgrammingError:
|
||||
pass
|
||||
except OperationalError:
|
||||
pass
|
||||
|
||||
if not site_url:
|
||||
# Otherwise, try to use the current request
|
||||
request = kwargs.get('request', None)
|
||||
|
||||
if request:
|
||||
site_url = request.build_absolute_uri('/')
|
||||
|
||||
if not site_url:
|
||||
# No site URL available, return the relative URL
|
||||
return relative_url
|
||||
|
||||
# Strip trailing slash from base url
|
||||
if site_url.endswith('/'):
|
||||
site_url = site_url[:-1]
|
||||
|
||||
if relative_url.startswith('/'):
|
||||
relative_url = relative_url[1:]
|
||||
|
||||
return f"{site_url}/{relative_url}"
|
||||
|
||||
|
||||
def get_base_url(**kwargs):
|
||||
"""Return the base URL for the InvenTree server"""
|
||||
return construct_absolute_url('', **kwargs)
|
||||
|
||||
|
||||
def download_image_from_url(remote_url, timeout=2.5):
|
||||
"""Download an image file from a remote URL.
|
||||
|
||||
This is a potentially dangerous operation, so we must perform some checks:
|
||||
|
||||
- The remote URL is available
|
||||
- The Content-Length is provided, and is not too large
|
||||
- The file is a valid image file
|
||||
|
||||
Arguments:
|
||||
remote_url: The remote URL to retrieve image
|
||||
max_size: Maximum allowed image size (default = 1MB)
|
||||
timeout: Connection timeout in seconds (default = 5)
|
||||
|
||||
Returns:
|
||||
An in-memory PIL image file, if the download was successful
|
||||
|
||||
Raises:
|
||||
requests.exceptions.ConnectionError: Connection could not be established
|
||||
requests.exceptions.Timeout: Connection timed out
|
||||
requests.exceptions.HTTPError: Server responded with invalid response code
|
||||
ValueError: Server responded with invalid 'Content-Length' value
|
||||
TypeError: Response is not a valid image
|
||||
"""
|
||||
|
||||
# Check that the provided URL at least looks valid
|
||||
validator = URLValidator()
|
||||
validator(remote_url)
|
||||
|
||||
# Calculate maximum allowable image size (in bytes)
|
||||
max_size = int(common.models.InvenTreeSetting.get_setting('INVENTREE_DOWNLOAD_IMAGE_MAX_SIZE')) * 1024 * 1024
|
||||
|
||||
# Add user specified user-agent to request (if specified)
|
||||
user_agent = common.models.InvenTreeSetting.get_setting('INVENTREE_DOWNLOAD_FROM_URL_USER_AGENT')
|
||||
if user_agent:
|
||||
headers = {"User-Agent": user_agent}
|
||||
else:
|
||||
headers = None
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
remote_url,
|
||||
timeout=timeout,
|
||||
allow_redirects=True,
|
||||
stream=True,
|
||||
headers=headers,
|
||||
)
|
||||
# Throw an error if anything goes wrong
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.ConnectionError as exc:
|
||||
raise Exception(_("Connection error") + f": {str(exc)}")
|
||||
except requests.exceptions.Timeout as exc:
|
||||
raise exc
|
||||
except requests.exceptions.HTTPError:
|
||||
raise requests.exceptions.HTTPError(_("Server responded with invalid status code") + f": {response.status_code}")
|
||||
except Exception as exc:
|
||||
raise Exception(_("Exception occurred") + f": {str(exc)}")
|
||||
|
||||
if response.status_code != 200:
|
||||
raise Exception(_("Server responded with invalid status code") + f": {response.status_code}")
|
||||
|
||||
try:
|
||||
content_length = int(response.headers.get('Content-Length', 0))
|
||||
except ValueError:
|
||||
raise ValueError(_("Server responded with invalid Content-Length value"))
|
||||
|
||||
if content_length > max_size:
|
||||
raise ValueError(_("Image size is too large"))
|
||||
|
||||
# Download the file, ensuring we do not exceed the reported size
|
||||
fo = io.BytesIO()
|
||||
|
||||
dl_size = 0
|
||||
chunk_size = 64 * 1024
|
||||
|
||||
for chunk in response.iter_content(chunk_size=chunk_size):
|
||||
dl_size += len(chunk)
|
||||
|
||||
if dl_size > max_size:
|
||||
raise ValueError(_("Image download exceeded maximum size"))
|
||||
|
||||
fo.write(chunk)
|
||||
|
||||
if dl_size == 0:
|
||||
raise ValueError(_("Remote server returned empty response"))
|
||||
|
||||
# Now, attempt to convert the downloaded data to a valid image file
|
||||
# img.verify() will throw an exception if the image is not valid
|
||||
try:
|
||||
img = Image.open(fo).convert()
|
||||
img.verify()
|
||||
except Exception:
|
||||
raise TypeError(_("Supplied URL is not a valid image file"))
|
||||
|
||||
return img
|
||||
|
||||
|
||||
def TestIfImage(img):
|
||||
"""Test if an image file is indeed an image."""
|
||||
try:
|
||||
@ -1016,120 +853,3 @@ def inheritors(cls):
|
||||
subcls.add(child)
|
||||
work.append(child)
|
||||
return subcls
|
||||
|
||||
|
||||
def notify_responsible(instance, sender, content: NotificationBody = InvenTreeNotificationBodies.NewOrder, exclude=None):
|
||||
"""Notify all responsible parties of a change in an instance.
|
||||
|
||||
Parses the supplied content with the provided instance and sender and sends a notification to all responsible users,
|
||||
excluding the optional excluded list.
|
||||
|
||||
Args:
|
||||
instance: The newly created instance
|
||||
sender: Sender model reference
|
||||
content (NotificationBody, optional): _description_. Defaults to InvenTreeNotificationBodies.NewOrder.
|
||||
exclude (User, optional): User instance that should be excluded. Defaults to None.
|
||||
"""
|
||||
if instance.responsible is not None:
|
||||
# Setup context for notification parsing
|
||||
content_context = {
|
||||
'instance': str(instance),
|
||||
'verbose_name': sender._meta.verbose_name,
|
||||
'app_label': sender._meta.app_label,
|
||||
'model_name': sender._meta.model_name,
|
||||
}
|
||||
|
||||
# Setup notification context
|
||||
context = {
|
||||
'instance': instance,
|
||||
'name': content.name.format(**content_context),
|
||||
'message': content.message.format(**content_context),
|
||||
'link': InvenTree.helpers.construct_absolute_url(instance.get_absolute_url()),
|
||||
'template': {
|
||||
'html': content.template.format(**content_context),
|
||||
'subject': content.name.format(**content_context),
|
||||
}
|
||||
}
|
||||
|
||||
# Create notification
|
||||
trigger_notification(
|
||||
instance,
|
||||
content.slug.format(**content_context),
|
||||
targets=[instance.responsible],
|
||||
target_exclude=[exclude],
|
||||
context=context,
|
||||
)
|
||||
|
||||
|
||||
def render_currency(money, decimal_places=None, currency=None, include_symbol=True, min_decimal_places=None, max_decimal_places=None):
|
||||
"""Render a currency / Money object to a formatted string (e.g. for reports)
|
||||
|
||||
Arguments:
|
||||
money: The Money instance to be rendered
|
||||
decimal_places: The number of decimal places to render to. If unspecified, uses the PRICING_DECIMAL_PLACES setting.
|
||||
currency: Optionally convert to the specified currency
|
||||
include_symbol: Render with the appropriate currency symbol
|
||||
min_decimal_places: The minimum number of decimal places to render to. If unspecified, uses the PRICING_DECIMAL_PLACES_MIN setting.
|
||||
max_decimal_places: The maximum number of decimal places to render to. If unspecified, uses the PRICING_DECIMAL_PLACES setting.
|
||||
"""
|
||||
|
||||
if money in [None, '']:
|
||||
return '-'
|
||||
|
||||
if type(money) is not Money:
|
||||
return '-'
|
||||
|
||||
if currency is not None:
|
||||
# Attempt to convert to the provided currency
|
||||
# If cannot be done, leave the original
|
||||
try:
|
||||
money = convert_money(money, currency)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if decimal_places is None:
|
||||
decimal_places = common.models.InvenTreeSetting.get_setting('PRICING_DECIMAL_PLACES', 6)
|
||||
|
||||
if min_decimal_places is None:
|
||||
min_decimal_places = common.models.InvenTreeSetting.get_setting('PRICING_DECIMAL_PLACES_MIN', 0)
|
||||
|
||||
if max_decimal_places is None:
|
||||
max_decimal_places = common.models.InvenTreeSetting.get_setting('PRICING_DECIMAL_PLACES', 6)
|
||||
|
||||
value = Decimal(str(money.amount)).normalize()
|
||||
value = str(value)
|
||||
|
||||
if '.' in value:
|
||||
decimals = len(value.split('.')[-1])
|
||||
|
||||
decimals = max(decimals, min_decimal_places)
|
||||
decimals = min(decimals, decimal_places)
|
||||
|
||||
decimal_places = decimals
|
||||
else:
|
||||
decimal_places = max(decimal_places, 2)
|
||||
|
||||
decimal_places = max(decimal_places, max_decimal_places)
|
||||
|
||||
return moneyed.localization.format_money(
|
||||
money,
|
||||
decimal_places=decimal_places,
|
||||
include_symbol=include_symbol,
|
||||
)
|
||||
|
||||
|
||||
def getModelsWithMixin(mixin_class) -> list:
|
||||
"""Return a list of models that inherit from the given mixin class.
|
||||
|
||||
Args:
|
||||
mixin_class: The mixin class to search for
|
||||
|
||||
Returns:
|
||||
List of models that inherit from the given mixin class
|
||||
"""
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
db_models = [x.model_class() for x in ContentType.objects.all() if x is not None]
|
||||
|
||||
return [x for x in db_models if x is not None and issubclass(x, mixin_class)]
|
||||
|
293
InvenTree/InvenTree/helpers_model.py
Normal file
293
InvenTree/InvenTree/helpers_model.py
Normal file
@ -0,0 +1,293 @@
|
||||
"""Provides helper functions used throughout the InvenTree project that access the database."""
|
||||
|
||||
import io
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.validators import URLValidator
|
||||
from django.db.utils import OperationalError, ProgrammingError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import moneyed.localization
|
||||
import requests
|
||||
from djmoney.contrib.exchange.models import convert_money
|
||||
from djmoney.money import Money
|
||||
from PIL import Image
|
||||
|
||||
import common.models
|
||||
import InvenTree
|
||||
import InvenTree.helpers_model
|
||||
import InvenTree.version
|
||||
from common.notifications import (InvenTreeNotificationBodies,
|
||||
NotificationBody, trigger_notification)
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
def getSetting(key, backup_value=None):
|
||||
"""Shortcut for reading a setting value from the database."""
|
||||
return common.models.InvenTreeSetting.get_setting(key, backup_value=backup_value)
|
||||
|
||||
|
||||
def construct_absolute_url(*arg, **kwargs):
|
||||
"""Construct (or attempt to construct) an absolute URL from a relative URL.
|
||||
|
||||
This is useful when (for example) sending an email to a user with a link
|
||||
to something in the InvenTree web framework.
|
||||
A URL is constructed in the following order:
|
||||
1. If setings.SITE_URL is set (e.g. in the Django settings), use that
|
||||
2. If the InvenTree setting INVENTREE_BASE_URL is set, use that
|
||||
3. Otherwise, use the current request URL (if available)
|
||||
"""
|
||||
|
||||
relative_url = '/'.join(arg)
|
||||
|
||||
# If a site URL is provided, use that
|
||||
site_url = getattr(settings, 'SITE_URL', None)
|
||||
|
||||
if not site_url:
|
||||
# Otherwise, try to use the InvenTree setting
|
||||
try:
|
||||
site_url = common.models.InvenTreeSetting.get_setting('INVENTREE_BASE_URL', create=False, cache=False)
|
||||
except ProgrammingError:
|
||||
pass
|
||||
except OperationalError:
|
||||
pass
|
||||
|
||||
if not site_url:
|
||||
# Otherwise, try to use the current request
|
||||
request = kwargs.get('request', None)
|
||||
|
||||
if request:
|
||||
site_url = request.build_absolute_uri('/')
|
||||
|
||||
if not site_url:
|
||||
# No site URL available, return the relative URL
|
||||
return relative_url
|
||||
|
||||
# Strip trailing slash from base url
|
||||
if site_url.endswith('/'):
|
||||
site_url = site_url[:-1]
|
||||
|
||||
if relative_url.startswith('/'):
|
||||
relative_url = relative_url[1:]
|
||||
|
||||
return f"{site_url}/{relative_url}"
|
||||
|
||||
|
||||
def get_base_url(**kwargs):
|
||||
"""Return the base URL for the InvenTree server"""
|
||||
return construct_absolute_url('', **kwargs)
|
||||
|
||||
|
||||
def download_image_from_url(remote_url, timeout=2.5):
|
||||
"""Download an image file from a remote URL.
|
||||
|
||||
This is a potentially dangerous operation, so we must perform some checks:
|
||||
- The remote URL is available
|
||||
- The Content-Length is provided, and is not too large
|
||||
- The file is a valid image file
|
||||
|
||||
Arguments:
|
||||
remote_url: The remote URL to retrieve image
|
||||
max_size: Maximum allowed image size (default = 1MB)
|
||||
timeout: Connection timeout in seconds (default = 5)
|
||||
|
||||
Returns:
|
||||
An in-memory PIL image file, if the download was successful
|
||||
|
||||
Raises:
|
||||
requests.exceptions.ConnectionError: Connection could not be established
|
||||
requests.exceptions.Timeout: Connection timed out
|
||||
requests.exceptions.HTTPError: Server responded with invalid response code
|
||||
ValueError: Server responded with invalid 'Content-Length' value
|
||||
TypeError: Response is not a valid image
|
||||
"""
|
||||
|
||||
# Check that the provided URL at least looks valid
|
||||
validator = URLValidator()
|
||||
validator(remote_url)
|
||||
|
||||
# Calculate maximum allowable image size (in bytes)
|
||||
max_size = int(common.models.InvenTreeSetting.get_setting('INVENTREE_DOWNLOAD_IMAGE_MAX_SIZE')) * 1024 * 1024
|
||||
|
||||
# Add user specified user-agent to request (if specified)
|
||||
user_agent = common.models.InvenTreeSetting.get_setting('INVENTREE_DOWNLOAD_FROM_URL_USER_AGENT')
|
||||
if user_agent:
|
||||
headers = {"User-Agent": user_agent}
|
||||
else:
|
||||
headers = None
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
remote_url,
|
||||
timeout=timeout,
|
||||
allow_redirects=True,
|
||||
stream=True,
|
||||
headers=headers,
|
||||
)
|
||||
# Throw an error if anything goes wrong
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.ConnectionError as exc:
|
||||
raise Exception(_("Connection error") + f": {str(exc)}")
|
||||
except requests.exceptions.Timeout as exc:
|
||||
raise exc
|
||||
except requests.exceptions.HTTPError:
|
||||
raise requests.exceptions.HTTPError(_("Server responded with invalid status code") + f": {response.status_code}")
|
||||
except Exception as exc:
|
||||
raise Exception(_("Exception occurred") + f": {str(exc)}")
|
||||
|
||||
if response.status_code != 200:
|
||||
raise Exception(_("Server responded with invalid status code") + f": {response.status_code}")
|
||||
|
||||
try:
|
||||
content_length = int(response.headers.get('Content-Length', 0))
|
||||
except ValueError:
|
||||
raise ValueError(_("Server responded with invalid Content-Length value"))
|
||||
|
||||
if content_length > max_size:
|
||||
raise ValueError(_("Image size is too large"))
|
||||
|
||||
# Download the file, ensuring we do not exceed the reported size
|
||||
fo = io.BytesIO()
|
||||
|
||||
dl_size = 0
|
||||
chunk_size = 64 * 1024
|
||||
|
||||
for chunk in response.iter_content(chunk_size=chunk_size):
|
||||
dl_size += len(chunk)
|
||||
|
||||
if dl_size > max_size:
|
||||
raise ValueError(_("Image download exceeded maximum size"))
|
||||
|
||||
fo.write(chunk)
|
||||
|
||||
if dl_size == 0:
|
||||
raise ValueError(_("Remote server returned empty response"))
|
||||
|
||||
# Now, attempt to convert the downloaded data to a valid image file
|
||||
# img.verify() will throw an exception if the image is not valid
|
||||
try:
|
||||
img = Image.open(fo).convert()
|
||||
img.verify()
|
||||
except Exception:
|
||||
raise TypeError(_("Supplied URL is not a valid image file"))
|
||||
|
||||
return img
|
||||
|
||||
|
||||
def render_currency(money, decimal_places=None, currency=None, include_symbol=True, min_decimal_places=None, max_decimal_places=None):
|
||||
"""Render a currency / Money object to a formatted string (e.g. for reports)
|
||||
|
||||
Arguments:
|
||||
money: The Money instance to be rendered
|
||||
decimal_places: The number of decimal places to render to. If unspecified, uses the PRICING_DECIMAL_PLACES setting.
|
||||
currency: Optionally convert to the specified currency
|
||||
include_symbol: Render with the appropriate currency symbol
|
||||
min_decimal_places: The minimum number of decimal places to render to. If unspecified, uses the PRICING_DECIMAL_PLACES_MIN setting.
|
||||
max_decimal_places: The maximum number of decimal places to render to. If unspecified, uses the PRICING_DECIMAL_PLACES setting.
|
||||
"""
|
||||
|
||||
if money in [None, '']:
|
||||
return '-'
|
||||
|
||||
if type(money) is not Money:
|
||||
return '-'
|
||||
|
||||
if currency is not None:
|
||||
# Attempt to convert to the provided currency
|
||||
# If cannot be done, leave the original
|
||||
try:
|
||||
money = convert_money(money, currency)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if decimal_places is None:
|
||||
decimal_places = common.models.InvenTreeSetting.get_setting('PRICING_DECIMAL_PLACES', 6)
|
||||
|
||||
if min_decimal_places is None:
|
||||
min_decimal_places = common.models.InvenTreeSetting.get_setting('PRICING_DECIMAL_PLACES_MIN', 0)
|
||||
|
||||
if max_decimal_places is None:
|
||||
max_decimal_places = common.models.InvenTreeSetting.get_setting('PRICING_DECIMAL_PLACES', 6)
|
||||
|
||||
value = Decimal(str(money.amount)).normalize()
|
||||
value = str(value)
|
||||
|
||||
if '.' in value:
|
||||
decimals = len(value.split('.')[-1])
|
||||
|
||||
decimals = max(decimals, min_decimal_places)
|
||||
decimals = min(decimals, decimal_places)
|
||||
|
||||
decimal_places = decimals
|
||||
else:
|
||||
decimal_places = max(decimal_places, 2)
|
||||
|
||||
decimal_places = max(decimal_places, max_decimal_places)
|
||||
|
||||
return moneyed.localization.format_money(
|
||||
money,
|
||||
decimal_places=decimal_places,
|
||||
include_symbol=include_symbol,
|
||||
)
|
||||
|
||||
|
||||
def getModelsWithMixin(mixin_class) -> list:
|
||||
"""Return a list of models that inherit from the given mixin class.
|
||||
|
||||
Args:
|
||||
mixin_class: The mixin class to search for
|
||||
Returns:
|
||||
List of models that inherit from the given mixin class
|
||||
"""
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
db_models = [x.model_class() for x in ContentType.objects.all() if x is not None]
|
||||
|
||||
return [x for x in db_models if x is not None and issubclass(x, mixin_class)]
|
||||
|
||||
|
||||
def notify_responsible(instance, sender, content: NotificationBody = InvenTreeNotificationBodies.NewOrder, exclude=None):
|
||||
"""Notify all responsible parties of a change in an instance.
|
||||
|
||||
Parses the supplied content with the provided instance and sender and sends a notification to all responsible users,
|
||||
excluding the optional excluded list.
|
||||
|
||||
Args:
|
||||
instance: The newly created instance
|
||||
sender: Sender model reference
|
||||
content (NotificationBody, optional): _description_. Defaults to InvenTreeNotificationBodies.NewOrder.
|
||||
exclude (User, optional): User instance that should be excluded. Defaults to None.
|
||||
"""
|
||||
if instance.responsible is not None:
|
||||
# Setup context for notification parsing
|
||||
content_context = {
|
||||
'instance': str(instance),
|
||||
'verbose_name': sender._meta.verbose_name,
|
||||
'app_label': sender._meta.app_label,
|
||||
'model_name': sender._meta.model_name,
|
||||
}
|
||||
|
||||
# Setup notification context
|
||||
context = {
|
||||
'instance': instance,
|
||||
'name': content.name.format(**content_context),
|
||||
'message': content.message.format(**content_context),
|
||||
'link': InvenTree.helpers_model.construct_absolute_url(instance.get_absolute_url()),
|
||||
'template': {
|
||||
'html': content.template.format(**content_context),
|
||||
'subject': content.name.format(**content_context),
|
||||
}
|
||||
}
|
||||
|
||||
# Create notification
|
||||
trigger_notification(
|
||||
instance,
|
||||
content.slug.format(**content_context),
|
||||
targets=[instance.responsible],
|
||||
target_exclude=[exclude],
|
||||
context=context,
|
||||
)
|
@ -21,10 +21,10 @@ from error_report.models import Error
|
||||
from mptt.exceptions import InvalidMove
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
|
||||
import common.models
|
||||
import InvenTree.fields
|
||||
import InvenTree.format
|
||||
import InvenTree.helpers
|
||||
import InvenTree.helpers_model
|
||||
from InvenTree.sanitizer import sanitize_svg
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
@ -207,7 +207,9 @@ class ReferenceIndexingMixin(models.Model):
|
||||
if cls.REFERENCE_PATTERN_SETTING is None:
|
||||
return ''
|
||||
|
||||
return common.models.InvenTreeSetting.get_setting(cls.REFERENCE_PATTERN_SETTING, create=False).strip()
|
||||
# import at function level to prevent cyclic imports
|
||||
from common.models import InvenTreeSetting
|
||||
return InvenTreeSetting.get_setting(cls.REFERENCE_PATTERN_SETTING, create=False).strip()
|
||||
|
||||
@classmethod
|
||||
def get_reference_context(cls):
|
||||
@ -889,7 +891,7 @@ def after_error_logged(sender, instance: Error, created: bool, **kwargs):
|
||||
|
||||
users = get_user_model().objects.filter(is_staff=True)
|
||||
|
||||
link = InvenTree.helpers.construct_absolute_url(
|
||||
link = InvenTree.helpers_model.construct_absolute_url(
|
||||
reverse('admin:error_report_error_change', kwargs={'object_id': instance.pk})
|
||||
)
|
||||
|
||||
|
@ -24,7 +24,7 @@ from taggit.serializers import TaggitSerializer
|
||||
from common.models import InvenTreeSetting
|
||||
from common.settings import currency_code_default, currency_code_mappings
|
||||
from InvenTree.fields import InvenTreeRestURLField, InvenTreeURLField
|
||||
from InvenTree.helpers import download_image_from_url
|
||||
from InvenTree.helpers_model import download_image_from_url
|
||||
|
||||
|
||||
class InvenTreeMoneySerializer(MoneyField):
|
||||
|
@ -21,6 +21,7 @@ from djmoney.money import Money
|
||||
import InvenTree.conversion
|
||||
import InvenTree.format
|
||||
import InvenTree.helpers
|
||||
import InvenTree.helpers_model
|
||||
import InvenTree.tasks
|
||||
from common.models import InvenTreeSetting
|
||||
from common.settings import currency_codes
|
||||
@ -282,7 +283,7 @@ class TestHelpers(TestCase):
|
||||
"\\invalid-url"
|
||||
]:
|
||||
with self.assertRaises(django_exceptions.ValidationError):
|
||||
helpers.download_image_from_url(url)
|
||||
InvenTree.helpers_model.download_image_from_url(url)
|
||||
|
||||
def dl_helper(url, expected_error, timeout=2.5, retries=3):
|
||||
"""Helper function for unit testing downloads.
|
||||
@ -297,7 +298,7 @@ class TestHelpers(TestCase):
|
||||
while tries < retries:
|
||||
|
||||
try:
|
||||
helpers.download_image_from_url(url, timeout=timeout)
|
||||
InvenTree.helpers_model.download_image_from_url(url, timeout=timeout)
|
||||
break
|
||||
except Exception as exc:
|
||||
if type(exc) is expected_error:
|
||||
@ -323,20 +324,20 @@ class TestHelpers(TestCase):
|
||||
|
||||
# Attempt to download an image which is too large
|
||||
with self.assertRaises(ValueError):
|
||||
helpers.download_image_from_url(large_img, timeout=10)
|
||||
InvenTree.helpers_model.download_image_from_url(large_img, timeout=10)
|
||||
|
||||
# Increase allowable download size
|
||||
InvenTreeSetting.set_setting('INVENTREE_DOWNLOAD_IMAGE_MAX_SIZE', 5, change_user=None)
|
||||
|
||||
# Download a valid image (should not throw an error)
|
||||
helpers.download_image_from_url(large_img, timeout=10)
|
||||
InvenTree.helpers_model.download_image_from_url(large_img, timeout=10)
|
||||
|
||||
def test_model_mixin(self):
|
||||
"""Test the getModelsWithMixin function"""
|
||||
|
||||
from InvenTree.models import InvenTreeBarcodeMixin
|
||||
|
||||
models = helpers.getModelsWithMixin(InvenTreeBarcodeMixin)
|
||||
models = InvenTree.helpers_model.getModelsWithMixin(InvenTreeBarcodeMixin)
|
||||
|
||||
self.assertIn(Part, models)
|
||||
self.assertIn(StockLocation, models)
|
||||
|
@ -27,6 +27,7 @@ from build.validators import generate_next_build_reference, validate_build_order
|
||||
|
||||
import InvenTree.fields
|
||||
import InvenTree.helpers
|
||||
import InvenTree.helpers_model
|
||||
import InvenTree.models
|
||||
import InvenTree.ready
|
||||
import InvenTree.tasks
|
||||
@ -539,7 +540,7 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
||||
'name': name,
|
||||
'slug': 'build.completed',
|
||||
'message': _('A build order has been completed'),
|
||||
'link': InvenTree.helpers.construct_absolute_url(self.get_absolute_url()),
|
||||
'link': InvenTree.helpers_model.construct_absolute_url(self.get_absolute_url()),
|
||||
'template': {
|
||||
'html': 'email/build_order_completed.html',
|
||||
'subject': name,
|
||||
@ -1210,7 +1211,7 @@ def after_save_build(sender, instance: Build, created: bool, **kwargs):
|
||||
InvenTree.tasks.offload_task(build_tasks.check_build_stock, instance)
|
||||
|
||||
# Notify the responsible users that the build order has been created
|
||||
InvenTree.helpers.notify_responsible(instance, sender, exclude=instance.issued_by)
|
||||
InvenTree.helpers_model.notify_responsible(instance, sender, exclude=instance.issued_by)
|
||||
|
||||
|
||||
class BuildOrderAttachment(InvenTree.models.InvenTreeAttachment):
|
||||
|
@ -13,7 +13,7 @@ from plugin.events import trigger_event
|
||||
import common.notifications
|
||||
import build.models
|
||||
import InvenTree.email
|
||||
import InvenTree.helpers
|
||||
import InvenTree.helpers_model
|
||||
import InvenTree.tasks
|
||||
from InvenTree.status_codes import BuildStatus
|
||||
from InvenTree.ready import isImportingData
|
||||
@ -65,7 +65,7 @@ def check_build_stock(build: build.models.Build):
|
||||
# There is not sufficient stock for this part
|
||||
|
||||
lines.append({
|
||||
'link': InvenTree.helpers.construct_absolute_url(sub_part.get_absolute_url()),
|
||||
'link': InvenTree.helpers_model.construct_absolute_url(sub_part.get_absolute_url()),
|
||||
'part': sub_part,
|
||||
'in_stock': in_stock,
|
||||
'allocated': allocated,
|
||||
@ -89,7 +89,7 @@ def check_build_stock(build: build.models.Build):
|
||||
logger.info(f"Notifying users of stock required for build {build.pk}")
|
||||
|
||||
context = {
|
||||
'link': InvenTree.helpers.construct_absolute_url(build.get_absolute_url()),
|
||||
'link': InvenTree.helpers_model.construct_absolute_url(build.get_absolute_url()),
|
||||
'build': build,
|
||||
'part': build.part,
|
||||
'lines': lines,
|
||||
@ -122,7 +122,7 @@ def notify_overdue_build_order(bo: build.models.Build):
|
||||
'order': bo,
|
||||
'name': name,
|
||||
'message': _(f"Build order {bo} is now overdue"),
|
||||
'link': InvenTree.helpers.construct_absolute_url(
|
||||
'link': InvenTree.helpers_model.construct_absolute_url(
|
||||
bo.get_absolute_url(),
|
||||
),
|
||||
'template': {
|
||||
|
@ -7,7 +7,8 @@ from rest_framework import serializers
|
||||
from common.models import (InvenTreeSetting, InvenTreeUserSetting,
|
||||
NewsFeedEntry, NotesImage, NotificationMessage,
|
||||
ProjectCode)
|
||||
from InvenTree.helpers import construct_absolute_url, get_objectreference
|
||||
from InvenTree.helpers import get_objectreference
|
||||
from InvenTree.helpers_model import construct_absolute_url
|
||||
from InvenTree.serializers import (InvenTreeImageSerializerField,
|
||||
InvenTreeModelSerializer)
|
||||
|
||||
|
@ -11,7 +11,7 @@ from django.utils import timezone
|
||||
|
||||
import feedparser
|
||||
|
||||
from InvenTree.helpers import getModelsWithMixin
|
||||
from InvenTree.helpers_model import getModelsWithMixin
|
||||
from InvenTree.models import InvenTreeNotesMixin
|
||||
from InvenTree.tasks import ScheduledTask, scheduled_task
|
||||
|
||||
|
@ -15,7 +15,8 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import part.models
|
||||
import stock.models
|
||||
from InvenTree.helpers import get_base_url, normalize, validateFilterString
|
||||
from InvenTree.helpers import normalize, validateFilterString
|
||||
from InvenTree.helpers_model import get_base_url
|
||||
from InvenTree.models import MetadataMixin
|
||||
from plugin.registry import registry
|
||||
|
||||
|
@ -19,8 +19,8 @@ from company.models import SupplierPart
|
||||
from InvenTree.api import (APIDownloadMixin, AttachmentMixin,
|
||||
ListCreateDestroyAPIView, MetadataView, StatusView)
|
||||
from InvenTree.filters import SEARCH_ORDER_FILTER, SEARCH_ORDER_FILTER_ALIAS
|
||||
from InvenTree.helpers import (DownloadFile, construct_absolute_url,
|
||||
get_base_url, str2bool)
|
||||
from InvenTree.helpers import DownloadFile, str2bool
|
||||
from InvenTree.helpers_model import construct_absolute_url, get_base_url
|
||||
from InvenTree.mixins import (CreateAPI, ListAPI, ListCreateAPI,
|
||||
RetrieveUpdateDestroyAPI)
|
||||
from InvenTree.status_codes import (PurchaseOrderStatus, ReturnOrderLineStatus,
|
||||
|
@ -36,7 +36,8 @@ from company.models import Company, Contact, SupplierPart
|
||||
from InvenTree.exceptions import log_error
|
||||
from InvenTree.fields import (InvenTreeModelMoneyField, InvenTreeURLField,
|
||||
RoundingDecimalField)
|
||||
from InvenTree.helpers import decimal2string, getSetting, notify_responsible
|
||||
from InvenTree.helpers import decimal2string
|
||||
from InvenTree.helpers_model import getSetting, notify_responsible
|
||||
from InvenTree.models import (InvenTreeAttachment, InvenTreeBarcodeMixin,
|
||||
InvenTreeNotesMixin, MetadataMixin,
|
||||
ReferenceIndexingMixin)
|
||||
|
@ -5,7 +5,7 @@ from datetime import datetime, timedelta
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import common.notifications
|
||||
import InvenTree.helpers
|
||||
import InvenTree.helpers_model
|
||||
import order.models
|
||||
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus
|
||||
from InvenTree.tasks import ScheduledTask, scheduled_task
|
||||
@ -29,7 +29,7 @@ def notify_overdue_purchase_order(po: order.models.PurchaseOrder):
|
||||
'order': po,
|
||||
'name': name,
|
||||
'message': _(f'Purchase order {po} is now overdue'),
|
||||
'link': InvenTree.helpers.construct_absolute_url(
|
||||
'link': InvenTree.helpers_model.construct_absolute_url(
|
||||
po.get_absolute_url(),
|
||||
),
|
||||
'template': {
|
||||
@ -92,7 +92,7 @@ def notify_overdue_sales_order(so: order.models.SalesOrder):
|
||||
'order': so,
|
||||
'name': name,
|
||||
'message': _(f"Sales order {so} is now overdue"),
|
||||
'link': InvenTree.helpers.construct_absolute_url(
|
||||
'link': InvenTree.helpers_model.construct_absolute_url(
|
||||
so.get_absolute_url(),
|
||||
),
|
||||
'template': {
|
||||
|
@ -21,6 +21,7 @@ import common.notifications
|
||||
import common.settings
|
||||
import company.models
|
||||
import InvenTree.helpers
|
||||
import InvenTree.helpers_model
|
||||
import InvenTree.tasks
|
||||
import part.models
|
||||
import stock.models
|
||||
@ -41,7 +42,7 @@ def notify_low_stock(part: part.models.Part):
|
||||
'part': part,
|
||||
'name': name,
|
||||
'message': message,
|
||||
'link': InvenTree.helpers.construct_absolute_url(part.get_absolute_url()),
|
||||
'link': InvenTree.helpers_model.construct_absolute_url(part.get_absolute_url()),
|
||||
'template': {
|
||||
'html': 'email/low_stock_notification.html',
|
||||
'subject': name,
|
||||
|
@ -14,6 +14,7 @@ from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import InvenTree.helpers
|
||||
import InvenTree.helpers_model
|
||||
from common.models import ColorTheme, InvenTreeSetting, InvenTreeUserSetting
|
||||
from common.settings import currency_code_default
|
||||
from InvenTree import settings, version
|
||||
@ -105,7 +106,7 @@ def render_date(context, date_object):
|
||||
def render_currency(money, **kwargs):
|
||||
"""Render a currency / Money object"""
|
||||
|
||||
return InvenTree.helpers.render_currency(money, **kwargs)
|
||||
return InvenTree.helpers_model.render_currency(money, **kwargs)
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
@ -224,7 +225,7 @@ def inventree_splash(**kwargs):
|
||||
@register.simple_tag()
|
||||
def inventree_base_url(*args, **kwargs):
|
||||
"""Return the base URL of the InvenTree server"""
|
||||
return InvenTree.helpers.get_base_url()
|
||||
return InvenTree.helpers_model.get_base_url()
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
|
@ -7,6 +7,7 @@ import requests
|
||||
|
||||
import part.models
|
||||
import stock.models
|
||||
from InvenTree.helpers import generateTestKey
|
||||
from plugin.helpers import (MixinNotImplementedError, render_template,
|
||||
render_text)
|
||||
|
||||
@ -444,7 +445,6 @@ class PanelMixin:
|
||||
Returns:
|
||||
Array of panels
|
||||
"""
|
||||
import InvenTree.helpers
|
||||
|
||||
panels = []
|
||||
|
||||
@ -482,7 +482,7 @@ class PanelMixin:
|
||||
panel['slug'] = self.slug
|
||||
|
||||
# Add a 'key' for the panel, which is mostly guaranteed to be unique
|
||||
panel['key'] = InvenTree.helpers.generateTestKey(self.slug + panel.get('title', 'panel'))
|
||||
panel['key'] = generateTestKey(self.slug + panel.get('title', 'panel'))
|
||||
|
||||
panels.append(panel)
|
||||
|
||||
|
@ -11,7 +11,8 @@ import json
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from InvenTree.helpers import getModelsWithMixin, hash_barcode
|
||||
from InvenTree.helpers import hash_barcode
|
||||
from InvenTree.helpers_model import getModelsWithMixin
|
||||
from InvenTree.models import InvenTreeBarcodeMixin
|
||||
from plugin import InvenTreePlugin
|
||||
from plugin.mixins import BarcodeMixin
|
||||
|
@ -20,7 +20,8 @@ import common.models
|
||||
import order.models
|
||||
import part.models
|
||||
import stock.models
|
||||
from InvenTree.helpers import get_base_url, validateFilterString
|
||||
from InvenTree.helpers import validateFilterString
|
||||
from InvenTree.helpers_model import get_base_url
|
||||
from InvenTree.models import MetadataMixin
|
||||
from plugin.registry import registry
|
||||
|
||||
|
@ -8,6 +8,7 @@ from django.conf import settings
|
||||
from django.utils.safestring import SafeString, mark_safe
|
||||
|
||||
import InvenTree.helpers
|
||||
import InvenTree.helpers_model
|
||||
from common.models import InvenTreeSetting
|
||||
from company.models import Company
|
||||
from part.models import Part
|
||||
@ -205,11 +206,11 @@ def logo_image(**kwargs):
|
||||
def internal_link(link, text):
|
||||
"""Make a <a></a> href which points to an InvenTree URL.
|
||||
|
||||
Uses the InvenTree.helpers.construct_absolute_url function to build the URL.
|
||||
Uses the InvenTree.helpers_model.construct_absolute_url function to build the URL.
|
||||
"""
|
||||
text = str(text)
|
||||
|
||||
url = InvenTree.helpers.construct_absolute_url(link)
|
||||
url = InvenTree.helpers_model.construct_absolute_url(link)
|
||||
|
||||
# If the base URL is not set, just return the text
|
||||
if not url:
|
||||
@ -246,7 +247,7 @@ def divide(x, y):
|
||||
def render_currency(money, **kwargs):
|
||||
"""Render a currency / Money object"""
|
||||
|
||||
return InvenTree.helpers.render_currency(money, **kwargs)
|
||||
return InvenTree.helpers_model.render_currency(money, **kwargs)
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
|
Loading…
Reference in New Issue
Block a user