\d+)/', views.CategoryDetail.as_view(), name='category-detail'),
]
# URL list for part web interface
diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py
index 9b6e699541..da9b372257 100644
--- a/InvenTree/part/views.py
+++ b/InvenTree/part/views.py
@@ -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'),
- }
diff --git a/InvenTree/plugin/apps.py b/InvenTree/plugin/apps.py
index 405b8cfcd2..9fdd43e6c6 100644
--- a/InvenTree/plugin/apps.py
+++ b/InvenTree/plugin/apps.py
@@ -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
diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py
index f776bf95b8..53326ab286 100644
--- a/InvenTree/plugin/helpers.py
+++ b/InvenTree/plugin/helpers.py
@@ -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:
diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py
index 7443191cb6..98486dee38 100644
--- a/InvenTree/plugin/registry.py
+++ b/InvenTree/plugin/registry.py
@@ -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}')
diff --git a/InvenTree/report/tests.py b/InvenTree/report/tests.py
index 336f48d6fc..63128fb2c6 100644
--- a/InvenTree/report/tests.py
+++ b/InvenTree/report/tests.py
@@ -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()
diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html
index c239ec0d09..19964e8b29 100644
--- a/InvenTree/stock/templates/stock/item_base.html
+++ b/InvenTree/stock/templates/stock/item_base.html
@@ -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() {
diff --git a/InvenTree/stock/templates/stock/item_delete.html b/InvenTree/stock/templates/stock/item_delete.html
deleted file mode 100644
index 087e0b5179..0000000000
--- a/InvenTree/stock/templates/stock/item_delete.html
+++ /dev/null
@@ -1,15 +0,0 @@
-{% extends "modal_delete_form.html" %}
-
-{% load i18n %}
-{% load inventree_extras %}
-
-{% block pre_form_content %}
-
-
-{% trans "Are you sure you want to delete this stock item?" %}
-
-{% decimal item.quantity as qty %}
-{% blocktrans with full_name=item.part.full_name %}This will remove {{qty}} units of {{full_name}} from stock.{% endblocktrans %}
-
-
-{% endblock %}
diff --git a/InvenTree/stock/templates/stock/location.html b/InvenTree/stock/templates/stock/location.html
index b8a3a6cc29..5fd8312b2f 100644
--- a/InvenTree/stock/templates/stock/location.html
+++ b/InvenTree/stock/templates/stock/location.html
@@ -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 %}",
{
diff --git a/InvenTree/stock/templates/stock/location_delete.html b/InvenTree/stock/templates/stock/location_delete.html
deleted file mode 100644
index b4e4deb49d..0000000000
--- a/InvenTree/stock/templates/stock/location_delete.html
+++ /dev/null
@@ -1,34 +0,0 @@
-{% extends "modal_delete_form.html" %}
-
-{% load i18n %}
-{% load inventree_extras %}
-
-{% block pre_form_content %}
-
- {% trans "Are you sure you want to delete this stock location?" %}
-
-
-{% if location.children.all|length > 0 %}
-
- {% blocktrans with n=location.children.all|length %}This location contains {{ n }} child locations{% endblocktrans %}.
- {% 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 %}
-
-{% endif %}
-
-
-{% if location.stock_items.all|length > 0 %}
-
- {% blocktrans with n=location.stock_items.all|length %}This location contains {{ n }} stock items{% endblocktrans %}.
- {% 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 %}
-
-{% endif %}
-
-{% endblock %}
diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py
index f338116d72..6c4eec8d7e 100644
--- a/InvenTree/stock/urls.py
+++ b/InvenTree/stock/urls.py
@@ -7,7 +7,6 @@ from stock import views
location_urls = [
re_path(r'^(?P\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
diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py
index b16fd640de..267b8734d1 100644
--- a/InvenTree/stock/views.py
+++ b/InvenTree/stock/views.py
@@ -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')
diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js
index 707903c15a..1cc98fa57c 100644
--- a/InvenTree/templates/js/translated/part.js
+++ b/InvenTree/templates/js/translated/part.js
@@ -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 = `
+
+ {% trans "Are you sure you want to delete this part category?" %}
+
+ - {% trans "Any child categories will be moved to the parent of this category" %}
+ - {% trans "Any parts in this category will be moved to the parent of this category" %}
+
+
`;
+
+ constructForm(url, {
+ title: '{% trans "Delete Part Category" %}',
+ method: 'DELETE',
+ preFormContent: html,
+ onSuccess: function(response) {
+ handleFormSuccess(response, options);
+ }
+ });
}
diff --git a/InvenTree/templates/js/translated/stock.js b/InvenTree/templates/js/translated/stock.js
index 4af520eca4..825e1a8094 100644
--- a/InvenTree/templates/js/translated/stock.js
+++ b/InvenTree/templates/js/translated/stock.js
@@ -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 = `
+
+ {% trans "Are you sure you want to delete this stock location?" %}
+
+ - {% trans "Any child locations will be moved to the parent of this location" %}
+ - {% trans "Any stock items in this location will be moved to the parent of this location" %}
+
+
+ `;
+
+ 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 = `
+
+ {% trans "Are you sure you want to delete this stock item?" %}
+
`;
+
+ 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
*/
diff --git a/docker/production/docker-compose.yml b/docker/production/docker-compose.yml
index 7308e82b91..092e62f510 100644
--- a/docker/production/docker-compose.yml
+++ b/docker/production/docker-compose.yml
@@ -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)
diff --git a/requirements.txt b/requirements.txt
index 564b92d4f1..26bfca213b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -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
diff --git a/tasks.py b/tasks.py
index fdec7b4fcf..36e8a20678 100644
--- a/tasks.py
+++ b/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')