mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'unit-test-improvements'
This commit is contained in:
commit
e899e3dacf
@ -108,24 +108,6 @@ class HelperForm(forms.ModelForm):
|
||||
self.helper.layout = Layout(*layouts)
|
||||
|
||||
|
||||
class DeleteForm(forms.Form):
|
||||
"""Generic deletion form which provides simple user confirmation."""
|
||||
|
||||
confirm_delete = forms.BooleanField(
|
||||
required=False,
|
||||
initial=False,
|
||||
label=_('Confirm delete'),
|
||||
help_text=_('Confirm item deletion')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
fields = [
|
||||
'confirm_delete'
|
||||
]
|
||||
|
||||
|
||||
class EditUserForm(HelperForm):
|
||||
"""Form for editing user information."""
|
||||
|
||||
|
@ -18,12 +18,13 @@ import sys
|
||||
from datetime import datetime
|
||||
|
||||
import django.conf.locale
|
||||
from django.contrib.messages import constants as messages
|
||||
from django.core.files.storage import default_storage
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import moneyed
|
||||
import sentry_sdk
|
||||
import yaml
|
||||
from sentry_sdk.integrations.django import DjangoIntegration
|
||||
|
||||
from .config import get_base_dir, get_config_file, get_plugin_file, get_setting
|
||||
|
||||
@ -33,6 +34,9 @@ def _is_true(x):
|
||||
return str(x).strip().lower() in ['1', 'y', 'yes', 't', 'true']
|
||||
|
||||
|
||||
# Default Sentry DSN (can be overriden if user wants custom sentry integration)
|
||||
INVENTREE_DSN = 'https://3928ccdba1d34895abde28031fd00100@o378676.ingest.sentry.io/6494600'
|
||||
|
||||
# Determine if we are running in "test" mode e.g. "manage.py test"
|
||||
TESTING = 'test' in sys.argv
|
||||
# Are enviroment variables manipulated by tests? Needs to be set by testing code
|
||||
@ -297,12 +301,24 @@ AUTHENTICATION_BACKENDS = CONFIG.get('authentication_backends', [
|
||||
'allauth.account.auth_backends.AuthenticationBackend', # SSO login via external providers
|
||||
])
|
||||
|
||||
DEBUG_TOOLBAR_ENABLED = DEBUG and CONFIG.get('debug_toolbar', False)
|
||||
|
||||
# If the debug toolbar is enabled, add the modules
|
||||
if DEBUG and CONFIG.get('debug_toolbar', False): # pragma: no cover
|
||||
if DEBUG_TOOLBAR_ENABLED: # pragma: no cover
|
||||
logger.info("Running with DEBUG_TOOLBAR enabled")
|
||||
INSTALLED_APPS.append('debug_toolbar')
|
||||
MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware')
|
||||
|
||||
# Internal IP addresses allowed to see the debug toolbar
|
||||
INTERNAL_IPS = [
|
||||
'127.0.0.1',
|
||||
]
|
||||
|
||||
if DOCKER:
|
||||
# Internal IP addresses are different when running under docker
|
||||
hostname, ___, ips = socket.gethostbyname_ex(socket.gethostname())
|
||||
INTERNAL_IPS = [ip[: ip.rfind(".")] + ".1" for ip in ips] + ["127.0.0.1", "10.0.2.2"]
|
||||
|
||||
# Allow secure http developer server in debug mode
|
||||
if DEBUG:
|
||||
INSTALLED_APPS.append('sslserver')
|
||||
@ -349,6 +365,12 @@ TEMPLATES = [
|
||||
},
|
||||
]
|
||||
|
||||
if DEBUG_TOOLBAR_ENABLED:
|
||||
# Note that the APP_DIRS value must be set when using debug_toolbar
|
||||
# But this will kill template loading for plugins
|
||||
TEMPLATES[0]['APP_DIRS'] = True
|
||||
del TEMPLATES[0]['OPTIONS']['loaders']
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'EXCEPTION_HANDLER': 'InvenTree.exceptions.exception_handler',
|
||||
'DATETIME_FORMAT': '%Y-%m-%d %H:%M',
|
||||
@ -546,7 +568,7 @@ db_config['TEST'] = {
|
||||
|
||||
# Set collation option for mysql test database
|
||||
if 'mysql' in db_engine:
|
||||
db_config['TEST']['COLLATION'] = 'utf8_general_ci'
|
||||
db_config['TEST']['COLLATION'] = 'utf8_general_ci' # pragma: no cover
|
||||
|
||||
DATABASES = {
|
||||
'default': db_config
|
||||
@ -805,17 +827,6 @@ CRISPY_TEMPLATE_PACK = 'bootstrap4'
|
||||
# Use database transactions when importing / exporting data
|
||||
IMPORT_EXPORT_USE_TRANSACTIONS = True
|
||||
|
||||
# Internal IP addresses allowed to see the debug toolbar
|
||||
INTERNAL_IPS = [
|
||||
'127.0.0.1',
|
||||
]
|
||||
|
||||
MESSAGE_TAGS = {
|
||||
messages.SUCCESS: 'alert alert-block alert-success',
|
||||
messages.ERROR: 'alert alert-block alert-danger',
|
||||
messages.INFO: 'alert alert-block alert-info',
|
||||
}
|
||||
|
||||
SITE_ID = 1
|
||||
|
||||
# Load the allauth social backends
|
||||
@ -882,6 +893,26 @@ MARKDOWNIFY_WHITELIST_ATTRS = [
|
||||
|
||||
MARKDOWNIFY_BLEACH = False
|
||||
|
||||
# Error reporting
|
||||
SENTRY_ENABLED = get_setting('INVENTREE_SENTRY_ENABLED', CONFIG.get('sentry_enabled', False))
|
||||
SENTRY_DSN = get_setting('INVENTREE_SENTRY_DSN', CONFIG.get('sentry_dsn', INVENTREE_DSN))
|
||||
|
||||
if SENTRY_ENABLED and SENTRY_DSN: # pragma: no cover
|
||||
sentry_sdk.init(
|
||||
dsn=SENTRY_DSN,
|
||||
integrations=[DjangoIntegration(), ],
|
||||
traces_sample_rate=1.0 if DEBUG else 0.15,
|
||||
send_default_pii=True
|
||||
)
|
||||
inventree_tags = {
|
||||
'testing': TESTING,
|
||||
'docker': DOCKER,
|
||||
'debug': DEBUG,
|
||||
'remote': REMOTE_LOGIN,
|
||||
}
|
||||
for key, val in inventree_tags.items():
|
||||
sentry_sdk.set_tag(f'inventree_{key}', val)
|
||||
|
||||
# Maintenance mode
|
||||
MAINTENANCE_MODE_RETRY_AFTER = 60
|
||||
MAINTENANCE_MODE_STATE_BACKEND = 'maintenance_mode.backends.DefaultStorageBackend'
|
||||
@ -925,6 +956,6 @@ CUSTOM_LOGO = get_setting(
|
||||
)
|
||||
|
||||
# check that the logo-file exsists in media
|
||||
if CUSTOM_LOGO and not default_storage.exists(CUSTOM_LOGO):
|
||||
if CUSTOM_LOGO and not default_storage.exists(CUSTOM_LOGO): # pragma: no cover
|
||||
CUSTOM_LOGO = False
|
||||
logger.warning("The custom logo file could not be found in the default media storage")
|
||||
|
@ -188,10 +188,10 @@ if settings.DEBUG:
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
||||
# Debug toolbar access (only allowed in DEBUG mode)
|
||||
if 'debug_toolbar' in settings.INSTALLED_APPS: # pragma: no cover
|
||||
if settings.DEBUG_TOOLBAR_ENABLED:
|
||||
import debug_toolbar
|
||||
urlpatterns = [
|
||||
path('__debug/', include(debug_toolbar.urls)),
|
||||
path('__debug__/', include(debug_toolbar.urls)),
|
||||
] + urlpatterns
|
||||
|
||||
# Send any unknown URLs to the parts page
|
||||
|
@ -34,8 +34,7 @@ from common.settings import currency_code_default, currency_codes
|
||||
from part.models import PartCategory
|
||||
from users.models import RuleSet, check_user_role
|
||||
|
||||
from .forms import DeleteForm, EditUserForm, SetPasswordForm
|
||||
from .helpers import str2bool
|
||||
from .forms import EditUserForm, SetPasswordForm
|
||||
|
||||
|
||||
def auth_request(request):
|
||||
@ -510,74 +509,6 @@ class AjaxUpdateView(AjaxMixin, UpdateView):
|
||||
return self.renderJsonResponse(request, form, data)
|
||||
|
||||
|
||||
class AjaxDeleteView(AjaxMixin, UpdateView):
|
||||
"""An 'AJAXified DeleteView for removing an object from the DB.
|
||||
|
||||
- Returns a HTML object (not a form!) in JSON format (for delivery to a modal window)
|
||||
- Handles deletion
|
||||
"""
|
||||
|
||||
form_class = DeleteForm
|
||||
ajax_form_title = _("Delete Item")
|
||||
ajax_template_name = "modal_delete_form.html"
|
||||
context_object_name = 'item'
|
||||
|
||||
def get_object(self):
|
||||
"""Return object matched to the model of the calling class."""
|
||||
try:
|
||||
self.object = self.model.objects.get(pk=self.kwargs['pk'])
|
||||
except Exception:
|
||||
return None
|
||||
return self.object
|
||||
|
||||
def get_form(self):
|
||||
"""Returns a form instance for the form_class of the calling class."""
|
||||
return self.form_class(self.get_form_kwargs())
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Respond to GET request.
|
||||
|
||||
- Render a DELETE confirmation form to JSON
|
||||
- Return rendered form to client
|
||||
"""
|
||||
super(UpdateView, self).get(request, *args, **kwargs)
|
||||
|
||||
form = self.get_form()
|
||||
|
||||
context = self.get_context_data()
|
||||
|
||||
context[self.context_object_name] = self.get_object()
|
||||
|
||||
return self.renderJsonResponse(request, form, context=context)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Respond to POST request.
|
||||
|
||||
- DELETE the object
|
||||
- Render success message to JSON and return to client
|
||||
"""
|
||||
obj = self.get_object()
|
||||
pk = obj.id
|
||||
|
||||
form = self.get_form()
|
||||
|
||||
confirmed = str2bool(request.POST.get('confirm_delete', False))
|
||||
context = self.get_context_data()
|
||||
|
||||
if confirmed:
|
||||
obj.delete()
|
||||
else:
|
||||
form.add_error('confirm_delete', _('Check box to confirm item deletion'))
|
||||
context[self.context_object_name] = self.get_object()
|
||||
|
||||
data = {
|
||||
'id': pk,
|
||||
'form_valid': confirmed
|
||||
}
|
||||
|
||||
return self.renderJsonResponse(request, form, data=data, context=context)
|
||||
|
||||
|
||||
class EditUserView(AjaxUpdateView):
|
||||
"""View for editing user information."""
|
||||
|
||||
|
@ -24,7 +24,7 @@ class CommonConfig(AppConfig):
|
||||
try:
|
||||
import common.models
|
||||
|
||||
if common.models.InvenTreeSetting.get_setting('SERVER_RESTART_REQUIRED', backup_value=False, create=False):
|
||||
if common.models.InvenTreeSetting.get_setting('SERVER_RESTART_REQUIRED', backup_value=False, create=False, cache=False):
|
||||
logger.info("Clearing SERVER_RESTART_REQUIRED flag")
|
||||
common.models.InvenTreeSetting.set_setting('SERVER_RESTART_REQUIRED', False, None)
|
||||
except Exception:
|
||||
|
@ -21,7 +21,8 @@ from django.contrib.auth.models import Group, User
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.humanize.templatetags.humanize import naturaltime
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import AppRegistryNotReady, ValidationError
|
||||
from django.core.validators import MinValueValidator, URLValidator
|
||||
from django.db import models, transaction
|
||||
from django.db.utils import IntegrityError, OperationalError
|
||||
@ -69,11 +70,56 @@ class BaseInvenTreeSetting(models.Model):
|
||||
"""Enforce validation and clean before saving."""
|
||||
self.key = str(self.key).upper()
|
||||
|
||||
do_cache = kwargs.pop('cache', True)
|
||||
|
||||
self.clean(**kwargs)
|
||||
self.validate_unique(**kwargs)
|
||||
|
||||
# Update this setting in the cache
|
||||
if do_cache:
|
||||
self.save_to_cache()
|
||||
|
||||
super().save()
|
||||
|
||||
@property
|
||||
def cache_key(self):
|
||||
"""Generate a unique cache key for this settings object"""
|
||||
return self.__class__.create_cache_key(self.key, **self.get_kwargs())
|
||||
|
||||
def save_to_cache(self):
|
||||
"""Save this setting object to cache"""
|
||||
|
||||
ckey = self.cache_key
|
||||
|
||||
logger.debug(f"Saving setting '{ckey}' to cache")
|
||||
|
||||
try:
|
||||
cache.set(
|
||||
ckey,
|
||||
self,
|
||||
timeout=3600
|
||||
)
|
||||
except TypeError:
|
||||
# Some characters cause issues with caching; ignore and move on
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def create_cache_key(cls, setting_key, **kwargs):
|
||||
"""Create a unique cache key for a particular setting object.
|
||||
|
||||
The cache key uses the following elements to ensure the key is 'unique':
|
||||
- The name of the class
|
||||
- The unique KEY string
|
||||
- Any key:value kwargs associated with the particular setting type (e.g. user-id)
|
||||
"""
|
||||
|
||||
key = f"{str(cls.__name__)}:{setting_key}"
|
||||
|
||||
for k, v in kwargs.items():
|
||||
key += f"_{k}:{v}"
|
||||
|
||||
return key
|
||||
|
||||
@classmethod
|
||||
def allValues(cls, user=None, exclude_hidden=False):
|
||||
"""Return a dict of "all" defined global settings.
|
||||
@ -220,11 +266,12 @@ class BaseInvenTreeSetting(models.Model):
|
||||
|
||||
- Key is case-insensitive
|
||||
- Returns None if no match is made
|
||||
|
||||
First checks the cache to see if this object has recently been accessed,
|
||||
and returns the cached version if so.
|
||||
"""
|
||||
key = str(key).strip().upper()
|
||||
|
||||
settings = cls.objects.all()
|
||||
|
||||
filters = {
|
||||
'key__iexact': key,
|
||||
}
|
||||
@ -253,7 +300,25 @@ class BaseInvenTreeSetting(models.Model):
|
||||
if method is not None:
|
||||
filters['method'] = method
|
||||
|
||||
# Perform cache lookup by default
|
||||
do_cache = kwargs.pop('cache', True)
|
||||
|
||||
ckey = cls.create_cache_key(key, **kwargs)
|
||||
|
||||
if do_cache:
|
||||
try:
|
||||
# First attempt to find the setting object in the cache
|
||||
cached_setting = cache.get(ckey)
|
||||
|
||||
if cached_setting is not None:
|
||||
return cached_setting
|
||||
|
||||
except AppRegistryNotReady:
|
||||
# Cache is not ready yet
|
||||
do_cache = False
|
||||
|
||||
try:
|
||||
settings = cls.objects.all()
|
||||
setting = settings.filter(**filters).first()
|
||||
except (ValueError, cls.DoesNotExist):
|
||||
setting = None
|
||||
@ -282,6 +347,10 @@ class BaseInvenTreeSetting(models.Model):
|
||||
# It might be the case that the database isn't created yet
|
||||
pass
|
||||
|
||||
if setting and do_cache:
|
||||
# Cache this setting object
|
||||
setting.save_to_cache()
|
||||
|
||||
return setting
|
||||
|
||||
@classmethod
|
||||
@ -1507,11 +1576,6 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
|
||||
help_text=_('User'),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_setting_object(cls, key, user=None):
|
||||
"""Return setting object for provided user."""
|
||||
return super().get_setting_object(key, user=user)
|
||||
|
||||
def validate_unique(self, exclude=None, **kwargs):
|
||||
"""Return if the setting (including key) is unique."""
|
||||
return super().validate_unique(exclude=exclude, user=self.user)
|
||||
|
@ -12,7 +12,7 @@ def currency_code_default():
|
||||
from common.models import InvenTreeSetting
|
||||
|
||||
try:
|
||||
code = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY', create=False)
|
||||
code = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY', create=False, cache=False)
|
||||
except ProgrammingError: # pragma: no cover
|
||||
# database is not initialized yet
|
||||
code = ''
|
||||
|
@ -4,6 +4,8 @@ import json
|
||||
from datetime import timedelta
|
||||
from http import HTTPStatus
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.cache import cache
|
||||
from django.test import Client, TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
@ -45,10 +47,10 @@ class SettingsTest(InvenTreeTestCase):
|
||||
"""Test settings functions and properties."""
|
||||
# define settings to check
|
||||
instance_ref = 'INVENTREE_INSTANCE'
|
||||
instance_obj = InvenTreeSetting.get_setting_object(instance_ref)
|
||||
instance_obj = InvenTreeSetting.get_setting_object(instance_ref, cache=False)
|
||||
|
||||
stale_ref = 'STOCK_STALE_DAYS'
|
||||
stale_days = InvenTreeSetting.get_setting_object(stale_ref)
|
||||
stale_days = InvenTreeSetting.get_setting_object(stale_ref, cache=False)
|
||||
|
||||
report_size_obj = InvenTreeSetting.get_setting_object('REPORT_DEFAULT_PAGE_SIZE')
|
||||
report_test_obj = InvenTreeSetting.get_setting_object('REPORT_ENABLE_TEST_REPORT')
|
||||
@ -189,17 +191,72 @@ class SettingsTest(InvenTreeTestCase):
|
||||
if setting.default_value not in [True, False]:
|
||||
raise ValueError(f'Non-boolean default value specified for {key}') # pragma: no cover
|
||||
|
||||
def test_global_setting_caching(self):
|
||||
"""Test caching operations for the global settings class"""
|
||||
|
||||
key = 'PART_NAME_FORMAT'
|
||||
|
||||
cache_key = InvenTreeSetting.create_cache_key(key)
|
||||
self.assertEqual(cache_key, 'InvenTreeSetting:PART_NAME_FORMAT')
|
||||
|
||||
cache.clear()
|
||||
|
||||
self.assertIsNone(cache.get(cache_key))
|
||||
|
||||
# First request should set cache
|
||||
val = InvenTreeSetting.get_setting(key)
|
||||
self.assertEqual(cache.get(cache_key).value, val)
|
||||
|
||||
for val in ['A', '{{ part.IPN }}', 'C']:
|
||||
# Check that the cached value is updated whenever the setting is saved
|
||||
InvenTreeSetting.set_setting(key, val, None)
|
||||
self.assertEqual(cache.get(cache_key).value, val)
|
||||
self.assertEqual(InvenTreeSetting.get_setting(key), val)
|
||||
|
||||
def test_user_setting_caching(self):
|
||||
"""Test caching operation for the user settings class"""
|
||||
|
||||
cache.clear()
|
||||
|
||||
# Generate a number of new usesr
|
||||
for idx in range(5):
|
||||
get_user_model().objects.create(
|
||||
username=f"User_{idx}",
|
||||
password="hunter42",
|
||||
email="email@dot.com",
|
||||
)
|
||||
|
||||
key = 'SEARCH_PREVIEW_RESULTS'
|
||||
|
||||
# Check that the settings are correctly cached for each separate user
|
||||
for user in get_user_model().objects.all():
|
||||
setting = InvenTreeUserSetting.get_setting_object(key, user=user)
|
||||
cache_key = setting.cache_key
|
||||
self.assertEqual(cache_key, f"InvenTreeUserSetting:SEARCH_PREVIEW_RESULTS_user:{user.username}")
|
||||
InvenTreeUserSetting.set_setting(key, user.pk, None, user=user)
|
||||
self.assertIsNotNone(cache.get(cache_key))
|
||||
|
||||
# Iterate through a second time, ensure the values have been cached correctly
|
||||
for user in get_user_model().objects.all():
|
||||
value = InvenTreeUserSetting.get_setting(key, user=user)
|
||||
self.assertEqual(value, user.pk)
|
||||
|
||||
|
||||
class GlobalSettingsApiTest(InvenTreeAPITestCase):
|
||||
"""Tests for the global settings API."""
|
||||
|
||||
def setUp(self):
|
||||
"""Ensure cache is cleared as part of test setup"""
|
||||
cache.clear()
|
||||
return super().setUp()
|
||||
|
||||
def test_global_settings_api_list(self):
|
||||
"""Test list URL for global settings."""
|
||||
url = reverse('api-global-setting-list')
|
||||
|
||||
# Read out each of the global settings value, to ensure they are instantiated in the database
|
||||
for key in InvenTreeSetting.SETTINGS:
|
||||
InvenTreeSetting.get_setting_object(key)
|
||||
InvenTreeSetting.get_setting_object(key, cache=False)
|
||||
|
||||
response = self.get(url, expected_code=200)
|
||||
|
||||
@ -422,7 +479,8 @@ class UserSettingsApiTest(InvenTreeAPITestCase):
|
||||
"""Test a integer user setting value."""
|
||||
setting = InvenTreeUserSetting.get_setting_object(
|
||||
'SEARCH_PREVIEW_RESULTS',
|
||||
user=self.user
|
||||
user=self.user,
|
||||
cache=False,
|
||||
)
|
||||
|
||||
url = reverse('api-user-setting-detail', kwargs={'key': setting.key})
|
||||
|
@ -102,6 +102,14 @@ debug: True
|
||||
# and only if InvenTree is accessed from a local IP (127.0.0.1)
|
||||
debug_toolbar: False
|
||||
|
||||
# Set sentry_enabled to True to report errors back to the maintainers
|
||||
# Use the environment variable INVENTREE_SENTRY_ENABLED
|
||||
# sentry_enabled: True
|
||||
|
||||
# Set sentry_dsn to your custom DSN if you want to use your own instance for error reporting
|
||||
# Use the environment variable INVENTREE_SENTRY_DSN
|
||||
# sentry_dsn: https://custom@custom.ingest.sentry.io/custom
|
||||
|
||||
# Set this variable to True to enable InvenTree Plugins
|
||||
# Alternatively, use the environment variable INVENTREE_PLUGINS_ENABLED
|
||||
plugins_enabled: False
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -347,23 +347,18 @@
|
||||
|
||||
{% if category %}
|
||||
$("#cat-edit").click(function () {
|
||||
|
||||
editCategory({{ category.pk }});
|
||||
});
|
||||
|
||||
{% if category.parent %}
|
||||
var redirect = "{% url 'category-detail' category.parent.id %}";
|
||||
{% else %}
|
||||
var redirect = "{% url 'part-index' %}";
|
||||
{% endif %}
|
||||
|
||||
$('#cat-delete').click(function() {
|
||||
launchModalForm(
|
||||
"{% url 'category-delete' category.id %}",
|
||||
{
|
||||
redirect: redirect
|
||||
}
|
||||
);
|
||||
deletePartCategory({{ category.pk }}, {
|
||||
{% if category.parent %}
|
||||
redirect: "{% url 'category-detail' category.parent.id %}",
|
||||
{% else %}
|
||||
redirect: "{% url 'part-index' %}",
|
||||
{% endif %}
|
||||
});
|
||||
});
|
||||
|
||||
{% endif %}
|
||||
|
@ -1,32 +0,0 @@
|
||||
{% extends "modal_delete_form.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block pre_form_content %}
|
||||
|
||||
<div class='alert alert-block alert-danger'>
|
||||
{% trans "Are you sure you want to delete this part category?" %}
|
||||
</div>
|
||||
|
||||
{% if category.children.all|length > 0 %}
|
||||
<div class='alert alert-block alert-warning'>
|
||||
{% blocktrans with n=category.children.all|length %}This category contains {{ n }} child categories{% endblocktrans %}.<br>
|
||||
{% if category.parent %}
|
||||
{% blocktrans with category=category.parent.name %}If this category is deleted, these child categories will be moved to {{ category }}{% endblocktrans %}.
|
||||
{% else %}
|
||||
{% trans "If this category is deleted, these child categories will be moved to the top level part category" %}.
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if category.parts.all|length > 0 %}
|
||||
<div class='alert alert-block alert-warning'>
|
||||
{% blocktrans with n=category.parts.all|length %}This category contains {{ n }} parts{% endblocktrans %}.<br>
|
||||
{% if category.parent %}
|
||||
{% blocktrans with category=category.parent.name %}If this category is deleted, these parts will be moved to {{ category }}{% endblocktrans %}.
|
||||
{% else %}
|
||||
{% trans "If this category is deleted, these parts will be moved to the top level part category" %}.
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
@ -3,6 +3,7 @@
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.test import TestCase
|
||||
|
||||
@ -394,6 +395,9 @@ class PartSettingsTest(InvenTreeTestCase):
|
||||
|
||||
def make_part(self):
|
||||
"""Helper function to create a simple part."""
|
||||
|
||||
cache.clear()
|
||||
|
||||
part = Part.objects.create(
|
||||
name='Test Part',
|
||||
description='I am but a humble test part',
|
||||
@ -404,6 +408,9 @@ class PartSettingsTest(InvenTreeTestCase):
|
||||
|
||||
def test_defaults(self):
|
||||
"""Test that the default values for the part settings are correct."""
|
||||
|
||||
cache.clear()
|
||||
|
||||
self.assertTrue(part.settings.part_component_default())
|
||||
self.assertTrue(part.settings.part_purchaseable_default())
|
||||
self.assertFalse(part.settings.part_salable_default())
|
||||
@ -411,6 +418,9 @@ class PartSettingsTest(InvenTreeTestCase):
|
||||
|
||||
def test_initial(self):
|
||||
"""Test the 'initial' default values (no default values have been set)"""
|
||||
|
||||
cache.clear()
|
||||
|
||||
part = self.make_part()
|
||||
|
||||
self.assertTrue(part.component)
|
||||
|
@ -33,11 +33,7 @@ category_urls = [
|
||||
re_path(r'^subcategory/', views.PartIndex.as_view(template_name='part/subcategory.html'), name='category-index-subcategory'),
|
||||
|
||||
# Category detail views
|
||||
re_path(r'(?P<pk>\d+)/', include([
|
||||
re_path(r'^delete/', views.CategoryDelete.as_view(), name='category-delete'),
|
||||
# Anything else
|
||||
re_path(r'^.*$', views.CategoryDetail.as_view(), name='category-detail'),
|
||||
]))
|
||||
re_path(r'(?P<pk>\d+)/', views.CategoryDetail.as_view(), name='category-detail'),
|
||||
]
|
||||
|
||||
# URL list for part web interface
|
||||
|
@ -24,8 +24,8 @@ from common.models import InvenTreeSetting
|
||||
from common.views import FileManagementAjaxView, FileManagementFormView
|
||||
from company.models import SupplierPart
|
||||
from InvenTree.helpers import str2bool
|
||||
from InvenTree.views import (AjaxDeleteView, AjaxUpdateView, AjaxView,
|
||||
InvenTreeRoleMixin, QRCodeView)
|
||||
from InvenTree.views import (AjaxUpdateView, AjaxView, InvenTreeRoleMixin,
|
||||
QRCodeView)
|
||||
from order.models import PurchaseOrderLineItem
|
||||
from plugin.views import InvenTreePluginViewMixin
|
||||
from stock.models import StockItem, StockLocation
|
||||
@ -875,18 +875,3 @@ class CategoryDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
|
||||
context['starred'] = category.is_starred_by(self.request.user)
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class CategoryDelete(AjaxDeleteView):
|
||||
"""Delete view to delete a PartCategory."""
|
||||
model = PartCategory
|
||||
ajax_template_name = 'part/category_delete.html'
|
||||
ajax_form_title = _('Delete Part Category')
|
||||
context_object_name = 'category'
|
||||
success_url = '/part/'
|
||||
|
||||
def get_data(self):
|
||||
"""Return custom context data when the category is deleted"""
|
||||
return {
|
||||
'danger': _('Part category was deleted'),
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ class PluginAppConfig(AppConfig):
|
||||
# this is the first startup
|
||||
try:
|
||||
from common.models import InvenTreeSetting
|
||||
if InvenTreeSetting.get_setting('PLUGIN_ON_STARTUP', create=False):
|
||||
if InvenTreeSetting.get_setting('PLUGIN_ON_STARTUP', create=False, cache=False):
|
||||
# make sure all plugins are installed
|
||||
registry.install_plugin_file()
|
||||
except Exception: # pragma: no cover
|
||||
|
@ -111,6 +111,9 @@ def get_git_log(path):
|
||||
output = output.split('\n')
|
||||
except subprocess.CalledProcessError: # pragma: no cover
|
||||
pass
|
||||
except FileNotFoundError: # pragma: no cover
|
||||
# Most likely the system does not have 'git' installed
|
||||
pass
|
||||
|
||||
if not output:
|
||||
output = 7 * [''] # pragma: no cover
|
||||
@ -125,6 +128,9 @@ def check_git_version():
|
||||
output = str(subprocess.check_output(['git', '--version'], cwd=os.path.dirname(settings.BASE_DIR)), 'utf-8')
|
||||
except subprocess.CalledProcessError: # pragma: no cover
|
||||
return False
|
||||
except FileNotFoundError: # pragma: no cover
|
||||
# Most likely the system does not have 'git' installed
|
||||
return False
|
||||
|
||||
# process version string
|
||||
try:
|
||||
|
@ -227,6 +227,9 @@ class PluginsRegistry:
|
||||
except subprocess.CalledProcessError as error: # pragma: no cover
|
||||
logger.error(f'Ran into error while trying to install plugins!\n{str(error)}')
|
||||
return False
|
||||
except FileNotFoundError: # pragma: no cover
|
||||
# System most likely does not have 'git' installed
|
||||
return False
|
||||
|
||||
logger.info(f'plugin requirements were run\n{output}')
|
||||
|
||||
|
@ -4,6 +4,7 @@ import os
|
||||
import shutil
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.http.response import StreamingHttpResponse
|
||||
from django.urls import reverse
|
||||
|
||||
@ -33,6 +34,11 @@ class ReportTest(InvenTreeAPITestCase):
|
||||
detail_url = None
|
||||
print_url = None
|
||||
|
||||
def setUp(self):
|
||||
"""Ensure cache is cleared as part of test setup"""
|
||||
cache.clear()
|
||||
return super().setUp()
|
||||
|
||||
def copyReportTemplate(self, filename, description):
|
||||
"""Copy the provided report template into the required media directory."""
|
||||
src_dir = os.path.join(
|
||||
@ -204,7 +210,7 @@ class BuildReportTest(ReportTest):
|
||||
self.assertEqual(headers['Content-Disposition'], 'attachment; filename="report.pdf"')
|
||||
|
||||
# Now, set the download type to be "inline"
|
||||
inline = InvenTreeUserSetting.get_setting_object('REPORT_INLINE', self.user)
|
||||
inline = InvenTreeUserSetting.get_setting_object('REPORT_INLINE', user=self.user)
|
||||
inline.value = True
|
||||
inline.save()
|
||||
|
||||
|
@ -581,12 +581,9 @@ $('#stock-add').click(function() {
|
||||
});
|
||||
|
||||
$("#stock-delete").click(function () {
|
||||
launchModalForm(
|
||||
"{% url 'stock-item-delete' item.id %}",
|
||||
{
|
||||
redirect: "{% url 'part-detail' item.part.id %}"
|
||||
}
|
||||
);
|
||||
deleteStockItem({{ item.pk }}, {
|
||||
redirect: '{% url "part-detail" item.part.pk %}',
|
||||
});
|
||||
});
|
||||
|
||||
{% if item.part.can_convert %}
|
||||
@ -599,7 +596,6 @@ $("#stock-convert").click(function() {
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if item.in_stock %}
|
||||
$("#stock-assign-to-customer").click(function() {
|
||||
|
||||
|
@ -1,15 +0,0 @@
|
||||
{% extends "modal_delete_form.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block pre_form_content %}
|
||||
|
||||
<div class='alert alert-danger alert-block'>
|
||||
{% trans "Are you sure you want to delete this stock item?" %}
|
||||
<br>
|
||||
{% decimal item.quantity as qty %}
|
||||
{% blocktrans with full_name=item.part.full_name %}This will remove <strong>{{qty}}</strong> units of <strong>{{full_name}}</strong> from stock.{% endblocktrans %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
@ -291,14 +291,15 @@
|
||||
});
|
||||
|
||||
$('#location-delete').click(function() {
|
||||
launchModalForm("{% url 'stock-location-delete' location.id %}",
|
||||
{
|
||||
redirect: "{% url 'stock-index' %}"
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
{% if location %}
|
||||
deleteStockLocation({{ location.pk }}, {
|
||||
{% if location.parent %}
|
||||
redirect: '{% url "stock-location-detail" location.parent.pk %}',
|
||||
{% else %}
|
||||
redirect: '{% url "stock-index" %}',
|
||||
{% endif %}
|
||||
});
|
||||
});
|
||||
|
||||
function adjustLocationStock(action) {
|
||||
inventreeGet(
|
||||
@ -329,8 +330,6 @@
|
||||
adjustLocationStock('move');
|
||||
});
|
||||
|
||||
{% endif %}
|
||||
|
||||
$('#show-qr-code').click(function() {
|
||||
launchModalForm("{% url 'stock-location-qr' location.id %}",
|
||||
{
|
||||
|
@ -1,34 +0,0 @@
|
||||
{% extends "modal_delete_form.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block pre_form_content %}
|
||||
<div class='alert alert-block alert-danger'>
|
||||
{% trans "Are you sure you want to delete this stock location?" %}
|
||||
</div>
|
||||
|
||||
{% if location.children.all|length > 0 %}
|
||||
<div class='alert alert-block alert-warning'>
|
||||
{% blocktrans with n=location.children.all|length %}This location contains {{ n }} child locations{% endblocktrans %}.<br>
|
||||
{% if location.parent %}
|
||||
{% blocktrans with location=location.parent.name %}If this location is deleted, these child locations will be moved to {{ location }}{% endblocktrans %}.
|
||||
{% else %}
|
||||
{% trans "If this location is deleted, these child locations will be moved to the top level stock location" %}.
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if location.stock_items.all|length > 0 %}
|
||||
<div class='alert alert-block alert-warning'>
|
||||
{% blocktrans with n=location.stock_items.all|length %}This location contains {{ n }} stock items{% endblocktrans %}.<br>
|
||||
{% if location.parent %}
|
||||
{% blocktrans with location=location.parent.name %}If this location is deleted, these stock items will be moved to {{ location }}{% endblocktrans %}.
|
||||
{% else %}
|
||||
{% trans "If this location is deleted, these stock items will be moved to the top level stock location" %}.
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
@ -7,7 +7,6 @@ from stock import views
|
||||
location_urls = [
|
||||
|
||||
re_path(r'^(?P<pk>\d+)/', include([
|
||||
re_path(r'^delete/?', views.StockLocationDelete.as_view(), name='stock-location-delete'),
|
||||
re_path(r'^qr_code/?', views.StockLocationQRCode.as_view(), name='stock-location-qr'),
|
||||
|
||||
# Anything else - direct to the location detail view
|
||||
@ -18,7 +17,6 @@ location_urls = [
|
||||
|
||||
stock_item_detail_urls = [
|
||||
re_path(r'^convert/', views.StockItemConvert.as_view(), name='stock-item-convert'),
|
||||
re_path(r'^delete/', views.StockItemDelete.as_view(), name='stock-item-delete'),
|
||||
re_path(r'^qr_code/', views.StockItemQRCode.as_view(), name='stock-item-qr'),
|
||||
|
||||
# Anything else - direct to the item detail view
|
||||
|
@ -6,8 +6,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import DetailView, ListView
|
||||
|
||||
import common.settings
|
||||
from InvenTree.views import (AjaxDeleteView, AjaxUpdateView,
|
||||
InvenTreeRoleMixin, QRCodeView)
|
||||
from InvenTree.views import AjaxUpdateView, InvenTreeRoleMixin, QRCodeView
|
||||
from plugin.views import InvenTreePluginViewMixin
|
||||
|
||||
from . import forms as StockForms
|
||||
@ -163,29 +162,3 @@ class StockItemConvert(AjaxUpdateView):
|
||||
stock_item.convert_to_variant(variant, user=self.request.user)
|
||||
|
||||
return stock_item
|
||||
|
||||
|
||||
class StockLocationDelete(AjaxDeleteView):
|
||||
"""View to delete a StockLocation.
|
||||
|
||||
Presents a deletion confirmation form to the user
|
||||
"""
|
||||
|
||||
model = StockLocation
|
||||
success_url = '/stock'
|
||||
ajax_template_name = 'stock/location_delete.html'
|
||||
context_object_name = 'location'
|
||||
ajax_form_title = _('Delete Stock Location')
|
||||
|
||||
|
||||
class StockItemDelete(AjaxDeleteView):
|
||||
"""View to delete a StockItem.
|
||||
|
||||
Presents a deletion confirmation form to the user
|
||||
"""
|
||||
|
||||
model = StockItem
|
||||
success_url = '/stock/'
|
||||
ajax_template_name = 'stock/item_delete.html'
|
||||
context_object_name = 'item'
|
||||
ajax_form_title = _('Delete Stock Item')
|
||||
|
@ -21,6 +21,7 @@
|
||||
|
||||
/* exported
|
||||
deletePart,
|
||||
deletePartCategory,
|
||||
duplicateBom,
|
||||
duplicatePart,
|
||||
editCategory,
|
||||
@ -317,7 +318,31 @@ function editCategory(pk) {
|
||||
title: '{% trans "Edit Part Category" %}',
|
||||
reload: true,
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Delete a PartCategory via the API
|
||||
*/
|
||||
function deletePartCategory(pk, options={}) {
|
||||
var url = `/api/part/category/${pk}/`;
|
||||
|
||||
var html = `
|
||||
<div class='alert alert-block alert-danger'>
|
||||
{% trans "Are you sure you want to delete this part category?" %}
|
||||
<ul>
|
||||
<li>{% trans "Any child categories will be moved to the parent of this category" %}</li>
|
||||
<li>{% trans "Any parts in this category will be moved to the parent of this category" %}</li>
|
||||
</ul>
|
||||
</div>`;
|
||||
|
||||
constructForm(url, {
|
||||
title: '{% trans "Delete Part Category" %}',
|
||||
method: 'DELETE',
|
||||
preFormContent: html,
|
||||
onSuccess: function(response) {
|
||||
handleFormSuccess(response, options);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
@ -39,6 +39,8 @@
|
||||
assignStockToCustomer,
|
||||
createNewStockItem,
|
||||
createStockLocation,
|
||||
deleteStockItem,
|
||||
deleteStockLocation,
|
||||
duplicateStockItem,
|
||||
editStockItem,
|
||||
editStockLocation,
|
||||
@ -156,6 +158,34 @@ function createStockLocation(options={}) {
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Launch an API form to delete a StockLocation
|
||||
*/
|
||||
function deleteStockLocation(pk, options={}) {
|
||||
var url = `/api/stock/location/${pk}/`;
|
||||
|
||||
var html = `
|
||||
<div class='alert alert-block alert-danger'>
|
||||
{% trans "Are you sure you want to delete this stock location?" %}
|
||||
<ul>
|
||||
<li>{% trans "Any child locations will be moved to the parent of this location" %}</li>
|
||||
<li>{% trans "Any stock items in this location will be moved to the parent of this location" %}</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
constructForm(url, {
|
||||
title: '{% trans "Delete Stock Location" %}',
|
||||
method: 'DELETE',
|
||||
preFormContent: html,
|
||||
onSuccess: function(response) {
|
||||
handleFormSuccess(response, options);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
function stockItemFields(options={}) {
|
||||
var fields = {
|
||||
part: {
|
||||
@ -328,6 +358,28 @@ function duplicateStockItem(pk, options) {
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Launch a modal form to delete a given StockItem
|
||||
*/
|
||||
function deleteStockItem(pk, options={}) {
|
||||
var url = `/api/stock/${pk}/`;
|
||||
|
||||
var html = `
|
||||
<div class='alert alert-block alert-danger'>
|
||||
{% trans "Are you sure you want to delete this stock item?" %}
|
||||
</div>`;
|
||||
|
||||
constructForm(url, {
|
||||
method: 'DELETE',
|
||||
title: '{% trans "Delete Stock Item" %}',
|
||||
preFormContent: html,
|
||||
onSuccess: function(response) {
|
||||
handleFormSuccess(response, options);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Launch a modal form to edit a given StockItem
|
||||
*/
|
||||
|
@ -57,6 +57,18 @@ services:
|
||||
- inventree_data:/var/lib/postgresql/data/
|
||||
restart: unless-stopped
|
||||
|
||||
# redis acts as database cache manager
|
||||
inventree-cache:
|
||||
container_name: inventree-cache
|
||||
image: redis:7.0
|
||||
depends_on:
|
||||
- inventree-db
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- ${INVENTREE_CACHE_PORT:-6379}:6379
|
||||
restart: unless-stopped
|
||||
|
||||
# InvenTree web server services
|
||||
# Uses gunicorn as the web server
|
||||
inventree-server:
|
||||
@ -67,6 +79,7 @@ services:
|
||||
- 8000
|
||||
depends_on:
|
||||
- inventree-db
|
||||
- inventree-cache
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
@ -81,7 +94,6 @@ services:
|
||||
image: inventree/inventree:stable
|
||||
command: invoke worker
|
||||
depends_on:
|
||||
- inventree-db
|
||||
- inventree-server
|
||||
env_file:
|
||||
- .env
|
||||
@ -113,18 +125,6 @@ services:
|
||||
- inventree_data:/var/www
|
||||
restart: unless-stopped
|
||||
|
||||
# redis acts as database cache manager
|
||||
inventree-cache:
|
||||
container_name: inventree-cache
|
||||
image: redis:7.0
|
||||
depends_on:
|
||||
- inventree-db
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- ${INVENTREE_CACHE_PORT:-6379}:6379
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
# NOTE: Change /path/to/data to a directory on your local machine
|
||||
# Persistent data, stored external to the container(s)
|
||||
|
@ -47,5 +47,6 @@ pygments==2.7.4 # Syntax highlighting
|
||||
python-barcode[images]==0.13.1 # Barcode generator
|
||||
qrcode[pil]==6.1 # QR code generator
|
||||
rapidfuzz==0.7.6 # Fuzzy string matching
|
||||
sentry-sdk==1.5.12 # Error reporting (optional)
|
||||
tablib[xls,xlsx,yaml] # Support for XLS and XLSX formats
|
||||
weasyprint==55.0 # PDF generation library
|
||||
|
458
tasks.py
458
tasks.py
@ -26,238 +26,8 @@ def apps():
|
||||
]
|
||||
|
||||
|
||||
def localDir():
|
||||
"""Returns the directory of *THIS* file.
|
||||
|
||||
Used to ensure that the various scripts always run
|
||||
in the correct directory.
|
||||
"""
|
||||
return os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def managePyDir():
|
||||
"""Returns the directory of the manage.py file"""
|
||||
return os.path.join(localDir(), 'InvenTree')
|
||||
|
||||
|
||||
def managePyPath():
|
||||
"""Return the path of the manage.py file"""
|
||||
return os.path.join(managePyDir(), 'manage.py')
|
||||
|
||||
|
||||
def manage(c, cmd, pty: bool = False):
|
||||
"""Runs a given command against django's "manage.py" script.
|
||||
|
||||
Args:
|
||||
c: Command line context.
|
||||
cmd: Django command to run.
|
||||
pty (bool, optional): Run an interactive session. Defaults to False.
|
||||
"""
|
||||
c.run('cd "{path}" && python3 manage.py {cmd}'.format(
|
||||
path=managePyDir(),
|
||||
cmd=cmd
|
||||
), pty=pty)
|
||||
|
||||
|
||||
@task
|
||||
def plugins(c):
|
||||
"""Installs all plugins as specified in 'plugins.txt'"""
|
||||
from InvenTree.InvenTree.config import get_plugin_file
|
||||
|
||||
plugin_file = get_plugin_file()
|
||||
|
||||
print(f"Installing plugin packages from '{plugin_file}'")
|
||||
|
||||
# Install the plugins
|
||||
c.run(f"pip3 install --disable-pip-version-check -U -r '{plugin_file}'")
|
||||
|
||||
|
||||
@task(post=[plugins])
|
||||
def install(c):
|
||||
"""Installs required python packages"""
|
||||
print("Installing required python packages from 'requirements.txt'")
|
||||
|
||||
# Install required Python packages with PIP
|
||||
c.run('pip3 install --no-cache-dir --disable-pip-version-check -U -r requirements.txt')
|
||||
|
||||
|
||||
@task
|
||||
def setup_dev(c):
|
||||
"""Sets up everything needed for the dev enviroment"""
|
||||
print("Installing required python packages from 'requirements.txt'")
|
||||
|
||||
# Install required Python packages with PIP
|
||||
c.run('pip3 install -U -r requirements.txt')
|
||||
|
||||
# Install pre-commit hook
|
||||
c.run('pre-commit install')
|
||||
|
||||
# Update all the hooks
|
||||
c.run('pre-commit autoupdate')
|
||||
|
||||
|
||||
@task
|
||||
def shell(c):
|
||||
"""Open a python shell with access to the InvenTree database models."""
|
||||
manage(c, 'shell', pty=True)
|
||||
|
||||
|
||||
@task
|
||||
def superuser(c):
|
||||
"""Create a superuser/admin account for the database."""
|
||||
manage(c, 'createsuperuser', pty=True)
|
||||
|
||||
|
||||
@task
|
||||
def check(c):
|
||||
"""Check validity of django codebase"""
|
||||
manage(c, "check")
|
||||
|
||||
|
||||
@task
|
||||
def wait(c):
|
||||
"""Wait until the database connection is ready"""
|
||||
return manage(c, "wait_for_db")
|
||||
|
||||
|
||||
@task(pre=[wait])
|
||||
def worker(c):
|
||||
"""Run the InvenTree background worker process"""
|
||||
manage(c, 'qcluster', pty=True)
|
||||
|
||||
|
||||
@task
|
||||
def rebuild_models(c):
|
||||
"""Rebuild database models with MPTT structures"""
|
||||
manage(c, "rebuild_models", pty=True)
|
||||
|
||||
|
||||
@task
|
||||
def rebuild_thumbnails(c):
|
||||
"""Rebuild missing image thumbnails"""
|
||||
manage(c, "rebuild_thumbnails", pty=True)
|
||||
|
||||
|
||||
@task
|
||||
def clean_settings(c):
|
||||
"""Clean the setting tables of old settings"""
|
||||
manage(c, "clean_settings")
|
||||
|
||||
|
||||
@task(help={'mail': 'mail of the user whos MFA should be disabled'})
|
||||
def remove_mfa(c, mail=''):
|
||||
"""Remove MFA for a user"""
|
||||
if not mail:
|
||||
print('You must provide a users mail')
|
||||
|
||||
manage(c, f"remove_mfa {mail}")
|
||||
|
||||
|
||||
@task(post=[rebuild_models, rebuild_thumbnails])
|
||||
def migrate(c):
|
||||
"""Performs database migrations.
|
||||
|
||||
This is a critical step if the database schema have been altered!
|
||||
"""
|
||||
print("Running InvenTree database migrations...")
|
||||
print("========================================")
|
||||
|
||||
manage(c, "makemigrations")
|
||||
manage(c, "migrate --noinput")
|
||||
manage(c, "migrate --run-syncdb")
|
||||
manage(c, "check")
|
||||
|
||||
print("========================================")
|
||||
print("InvenTree database migrations completed!")
|
||||
|
||||
|
||||
@task
|
||||
def static(c):
|
||||
"""Copies required static files to the STATIC_ROOT directory, as per Django requirements."""
|
||||
manage(c, "prerender")
|
||||
manage(c, "collectstatic --no-input")
|
||||
|
||||
|
||||
@task
|
||||
def translate_stats(c):
|
||||
"""Collect translation stats.
|
||||
|
||||
The file generated from this is needed for the UI.
|
||||
"""
|
||||
path = os.path.join('InvenTree', 'script', 'translation_stats.py')
|
||||
c.run(f'python3 {path}')
|
||||
|
||||
|
||||
@task(post=[translate_stats, static])
|
||||
def translate(c):
|
||||
"""Rebuild translation source files. (Advanced use only!)
|
||||
|
||||
Note: This command should not be used on a local install,
|
||||
it is performed as part of the InvenTree translation toolchain.
|
||||
"""
|
||||
# Translate applicable .py / .html / .js files
|
||||
manage(c, "makemessages --all -e py,html,js --no-wrap")
|
||||
manage(c, "compilemessages")
|
||||
|
||||
|
||||
@task(pre=[install, migrate, static, clean_settings])
|
||||
def update(c):
|
||||
"""Update InvenTree installation.
|
||||
|
||||
This command should be invoked after source code has been updated,
|
||||
e.g. downloading new code from GitHub.
|
||||
|
||||
The following tasks are performed, in order:
|
||||
|
||||
- install
|
||||
- migrate
|
||||
- translate_stats
|
||||
- static
|
||||
- clean_settings
|
||||
"""
|
||||
# Recompile the translation files (.mo)
|
||||
# We do not run 'invoke translate' here, as that will touch the source (.po) files too!
|
||||
manage(c, 'compilemessages', pty=True)
|
||||
|
||||
|
||||
@task
|
||||
def style(c):
|
||||
"""Run PEP style checks against InvenTree sourcecode"""
|
||||
print("Running PEP style checks...")
|
||||
c.run('flake8 InvenTree tasks.py')
|
||||
|
||||
|
||||
@task
|
||||
def test(c, database=None):
|
||||
"""Run unit-tests for InvenTree codebase."""
|
||||
# Run sanity check on the django install
|
||||
manage(c, 'check')
|
||||
|
||||
# Run coverage tests
|
||||
manage(c, 'test', pty=True)
|
||||
|
||||
|
||||
@task
|
||||
def coverage(c):
|
||||
"""Run code-coverage of the InvenTree codebase, using the 'coverage' code-analysis tools.
|
||||
|
||||
Generates a code coverage report (available in the htmlcov directory)
|
||||
"""
|
||||
# Run sanity check on the django install
|
||||
manage(c, 'check')
|
||||
|
||||
# Run coverage tests
|
||||
c.run('coverage run {manage} test {apps}'.format(
|
||||
manage=managePyPath(),
|
||||
apps=' '.join(apps())
|
||||
))
|
||||
|
||||
# Generate coverage report
|
||||
c.run('coverage html')
|
||||
|
||||
|
||||
def content_excludes():
|
||||
"""Returns a list of content types to exclude from import/export"""
|
||||
"""Returns a list of content types to exclude from import/export."""
|
||||
excludes = [
|
||||
"contenttypes",
|
||||
"auth.permission",
|
||||
@ -282,6 +52,179 @@ def content_excludes():
|
||||
return output
|
||||
|
||||
|
||||
def localDir():
|
||||
"""Returns the directory of *THIS* file.
|
||||
|
||||
Used to ensure that the various scripts always run
|
||||
in the correct directory.
|
||||
"""
|
||||
return os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def managePyDir():
|
||||
"""Returns the directory of the manage.py file."""
|
||||
return os.path.join(localDir(), 'InvenTree')
|
||||
|
||||
|
||||
def managePyPath():
|
||||
"""Return the path of the manage.py file."""
|
||||
return os.path.join(managePyDir(), 'manage.py')
|
||||
|
||||
|
||||
def manage(c, cmd, pty: bool = False):
|
||||
"""Runs a given command against django's "manage.py" script.
|
||||
|
||||
Args:
|
||||
c: Command line context.
|
||||
cmd: Django command to run.
|
||||
pty (bool, optional): Run an interactive session. Defaults to False.
|
||||
"""
|
||||
c.run('cd "{path}" && python3 manage.py {cmd}'.format(
|
||||
path=managePyDir(),
|
||||
cmd=cmd
|
||||
), pty=pty)
|
||||
|
||||
|
||||
# Install tasks
|
||||
@task
|
||||
def plugins(c):
|
||||
"""Installs all plugins as specified in 'plugins.txt'."""
|
||||
from InvenTree.InvenTree.config import get_plugin_file
|
||||
|
||||
plugin_file = get_plugin_file()
|
||||
|
||||
print(f"Installing plugin packages from '{plugin_file}'")
|
||||
|
||||
# Install the plugins
|
||||
c.run(f"pip3 install --disable-pip-version-check -U -r '{plugin_file}'")
|
||||
|
||||
|
||||
@task(post=[plugins])
|
||||
def install(c):
|
||||
"""Installs required python packages."""
|
||||
print("Installing required python packages from 'requirements.txt'")
|
||||
|
||||
# Install required Python packages with PIP
|
||||
c.run('pip3 install --no-cache-dir --disable-pip-version-check -U -r requirements.txt')
|
||||
|
||||
|
||||
@task
|
||||
def setup_dev(c):
|
||||
"""Sets up everything needed for the dev enviroment."""
|
||||
print("Installing required python packages from 'requirements.txt'")
|
||||
|
||||
# Install required Python packages with PIP
|
||||
c.run('pip3 install -U -r requirements.txt')
|
||||
|
||||
# Install pre-commit hook
|
||||
c.run('pre-commit install')
|
||||
|
||||
# Update all the hooks
|
||||
c.run('pre-commit autoupdate')
|
||||
|
||||
|
||||
# Setup / maintenance tasks
|
||||
@task
|
||||
def superuser(c):
|
||||
"""Create a superuser/admin account for the database."""
|
||||
manage(c, 'createsuperuser', pty=True)
|
||||
|
||||
|
||||
@task
|
||||
def rebuild_models(c):
|
||||
"""Rebuild database models with MPTT structures."""
|
||||
manage(c, "rebuild_models", pty=True)
|
||||
|
||||
|
||||
@task
|
||||
def rebuild_thumbnails(c):
|
||||
"""Rebuild missing image thumbnails."""
|
||||
manage(c, "rebuild_thumbnails", pty=True)
|
||||
|
||||
|
||||
@task
|
||||
def clean_settings(c):
|
||||
"""Clean the setting tables of old settings."""
|
||||
manage(c, "clean_settings")
|
||||
|
||||
|
||||
@task(help={'mail': 'mail of the user whos MFA should be disabled'})
|
||||
def remove_mfa(c, mail=''):
|
||||
"""Remove MFA for a user."""
|
||||
if not mail:
|
||||
print('You must provide a users mail')
|
||||
|
||||
manage(c, f"remove_mfa {mail}")
|
||||
|
||||
|
||||
@task
|
||||
def static(c):
|
||||
"""Copies required static files to the STATIC_ROOT directory, as per Django requirements."""
|
||||
manage(c, "prerender")
|
||||
manage(c, "collectstatic --no-input")
|
||||
|
||||
|
||||
@task
|
||||
def translate_stats(c):
|
||||
"""Collect translation stats.
|
||||
|
||||
The file generated from this is needed for the UI.
|
||||
"""
|
||||
path = os.path.join('InvenTree', 'script', 'translation_stats.py')
|
||||
c.run(f'python3 {path}')
|
||||
|
||||
|
||||
@task(post=[translate_stats, static])
|
||||
def translate(c):
|
||||
"""Rebuild translation source files. Advanced use only!
|
||||
|
||||
Note: This command should not be used on a local install,
|
||||
it is performed as part of the InvenTree translation toolchain.
|
||||
"""
|
||||
# Translate applicable .py / .html / .js files
|
||||
manage(c, "makemessages --all -e py,html,js --no-wrap")
|
||||
manage(c, "compilemessages")
|
||||
|
||||
|
||||
@task(post=[rebuild_models, rebuild_thumbnails])
|
||||
def migrate(c):
|
||||
"""Performs database migrations.
|
||||
|
||||
This is a critical step if the database schema have been altered!
|
||||
"""
|
||||
print("Running InvenTree database migrations...")
|
||||
print("========================================")
|
||||
|
||||
manage(c, "makemigrations")
|
||||
manage(c, "migrate --noinput")
|
||||
manage(c, "migrate --run-syncdb")
|
||||
manage(c, "check")
|
||||
|
||||
print("========================================")
|
||||
print("InvenTree database migrations completed!")
|
||||
|
||||
|
||||
@task(pre=[install, migrate, static, clean_settings, translate_stats])
|
||||
def update(c):
|
||||
"""Update InvenTree installation.
|
||||
|
||||
This command should be invoked after source code has been updated,
|
||||
e.g. downloading new code from GitHub.
|
||||
|
||||
The following tasks are performed, in order:
|
||||
|
||||
- install
|
||||
- migrate
|
||||
- static
|
||||
- clean_settings
|
||||
- translate_stats
|
||||
"""
|
||||
# Recompile the translation files (.mo)
|
||||
# We do not run 'invoke translate' here, as that will touch the source (.po) files too!
|
||||
manage(c, 'compilemessages', pty=True)
|
||||
|
||||
|
||||
# Data tasks
|
||||
@task(help={
|
||||
'filename': "Output filename (default = 'data.json')",
|
||||
'overwrite': "Overwrite existing files without asking first (default = off/False)",
|
||||
@ -359,7 +302,7 @@ def export_records(c, filename='data.json', overwrite=False, include_permissions
|
||||
|
||||
@task(help={'filename': 'Input filename', 'clear': 'Clear existing data before import'}, post=[rebuild_models, rebuild_thumbnails])
|
||||
def import_records(c, filename='data.json', clear=False):
|
||||
"""Import database records from a file"""
|
||||
"""Import database records from a file."""
|
||||
# Get an absolute path to the supplied filename
|
||||
if not os.path.isabs(filename):
|
||||
filename = os.path.join(localDir(), filename)
|
||||
@ -462,6 +405,7 @@ def import_fixtures(c):
|
||||
manage(c, command, pty=True)
|
||||
|
||||
|
||||
# Execution tasks
|
||||
@task(help={'address': 'Server address:port (default=127.0.0.1:8000)'})
|
||||
def server(c, address="127.0.0.1:8000"):
|
||||
"""Launch a (deveopment) server using Django's in-built webserver.
|
||||
@ -471,9 +415,28 @@ def server(c, address="127.0.0.1:8000"):
|
||||
manage(c, "runserver {address}".format(address=address), pty=True)
|
||||
|
||||
|
||||
@task
|
||||
def wait(c):
|
||||
"""Wait until the database connection is ready."""
|
||||
return manage(c, "wait_for_db")
|
||||
|
||||
|
||||
@task(pre=[wait])
|
||||
def worker(c):
|
||||
"""Run the InvenTree background worker process."""
|
||||
manage(c, 'qcluster', pty=True)
|
||||
|
||||
|
||||
# Testing tasks
|
||||
@task
|
||||
def render_js_files(c):
|
||||
"""Render templated javascript files (used for static testing)."""
|
||||
manage(c, "test InvenTree.ci_render_js")
|
||||
|
||||
|
||||
@task(post=[translate_stats, static, server])
|
||||
def test_translations(c):
|
||||
"""Add a fictional language to test if each component is ready for translations"""
|
||||
"""Add a fictional language to test if each component is ready for translations."""
|
||||
import django
|
||||
from django.conf import settings
|
||||
|
||||
@ -539,6 +502,29 @@ def test_translations(c):
|
||||
|
||||
|
||||
@task
|
||||
def render_js_files(c):
|
||||
"""Render templated javascript files (used for static testing)."""
|
||||
manage(c, "test InvenTree.ci_render_js")
|
||||
def test(c, database=None):
|
||||
"""Run unit-tests for InvenTree codebase."""
|
||||
# Run sanity check on the django install
|
||||
manage(c, 'check')
|
||||
|
||||
# Run coverage tests
|
||||
manage(c, 'test', pty=True)
|
||||
|
||||
|
||||
@task
|
||||
def coverage(c):
|
||||
"""Run code-coverage of the InvenTree codebase, using the 'coverage' code-analysis tools.
|
||||
|
||||
Generates a code coverage report (available in the htmlcov directory)
|
||||
"""
|
||||
# Run sanity check on the django install
|
||||
manage(c, 'check')
|
||||
|
||||
# Run coverage tests
|
||||
c.run('coverage run {manage} test {apps}'.format(
|
||||
manage=managePyPath(),
|
||||
apps=' '.join(apps())
|
||||
))
|
||||
|
||||
# Generate coverage report
|
||||
c.run('coverage html')
|
||||
|
Loading…
Reference in New Issue
Block a user