2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-05-23 09:35:30 +00:00

Merge remote-tracking branch 'matmair/master'

This commit is contained in:
Oliver Walters
2022-08-08 07:57:09 +10:00
50 changed files with 12300 additions and 11648 deletions
+22
View File
@@ -0,0 +1,22 @@
name: Update dependency files regularly
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * *"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup
run: pip install -r requirements-dev.txt
- name: Update requirements.txt
run: pip-compile --output-file=requirements.txt requirements.in -U
- name: Update requirements-dev.txt
run: pip-compile --generate-hashes --output-file=requirements-dev.txt requirements-dev.in -U
- uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: "[Bot] Updated dependency"
branch: dep-update
+4 -3
View File
@@ -7,12 +7,13 @@ tasks:
export INVENTREE_STATIC_ROOT='/workspace/InvenTree/dev/static' export INVENTREE_STATIC_ROOT='/workspace/InvenTree/dev/static'
export PIP_USER='no' export PIP_USER='no'
sudo apt install gettext sudo apt install -y gettext
python3 -m venv venv python3 -m venv venv
source venv/bin/activate source venv/bin/activate
pip install invoke pip install invoke pyyaml
mkdir dev mkdir dev
inv setup-test invoke update
invoke setup-test --ignore-update --path inventree-data
gp sync-done start_server gp sync-done start_server
- name: Start server - name: Start server
+5 -1
View File
@@ -2,11 +2,15 @@
# InvenTree API version # InvenTree API version
INVENTREE_API_VERSION = 69 INVENTREE_API_VERSION = 70
""" """
Increment this API version number whenever there is a significant change to the API that any clients need to know about Increment this API version number whenever there is a significant change to the API that any clients need to know about
v70 -> 2022-08-02 : https://github.com/inventree/InvenTree/pull/3451
- Adds a 'depth' parameter to the PartCategory list API
- Adds a 'depth' parameter to the StockLocation list API
v69 -> 2022-08-01 : https://github.com/inventree/InvenTree/pull/3443 v69 -> 2022-08-01 : https://github.com/inventree/InvenTree/pull/3443
- Updates the PartCategory list API: - Updates the PartCategory list API:
- Improve query efficiency: O(n) becomes O(1) - Improve query efficiency: O(n) becomes O(1)
+14 -13
View File
@@ -6,7 +6,6 @@ from decimal import Decimal
from django import forms from django import forms
from django.core import validators from django.core import validators
from django.db import models as models from django.db import models as models
from django.forms.fields import URLField as FormURLField
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from djmoney.forms.fields import MoneyField from djmoney.forms.fields import MoneyField
@@ -23,26 +22,28 @@ class InvenTreeRestURLField(RestURLField):
"""Custom field for DRF with custom scheme vaildators.""" """Custom field for DRF with custom scheme vaildators."""
def __init__(self, **kwargs): def __init__(self, **kwargs):
"""Update schemes.""" """Update schemes."""
# Enforce 'max length' parameter in form validation
if 'max_length' not in kwargs:
kwargs['max_length'] = 200
super().__init__(**kwargs) super().__init__(**kwargs)
self.validators[-1].schemes = allowable_url_schemes() self.validators[-1].schemes = allowable_url_schemes()
class InvenTreeURLFormField(FormURLField):
"""Custom URL form field with custom scheme validators."""
default_validators = [validators.URLValidator(schemes=allowable_url_schemes())]
class InvenTreeURLField(models.URLField): class InvenTreeURLField(models.URLField):
"""Custom URL field which has custom scheme validators.""" """Custom URL field which has custom scheme validators."""
validators = [validators.URLValidator(schemes=allowable_url_schemes())] default_validators = [validators.URLValidator(schemes=allowable_url_schemes())]
def formfield(self, **kwargs): def __init__(self, **kwargs):
"""Return a Field instance for this field.""" """Initialization method for InvenTreeURLField"""
return super().formfield(**{
'form_class': InvenTreeURLFormField # Max length for InvenTreeURLField defaults to 200
}) if 'max_length' not in kwargs:
kwargs['max_length'] = 200
super().__init__(**kwargs)
def money_kwargs(): def money_kwargs():
+31 -4
View File
@@ -12,6 +12,7 @@ from wsgiref.util import FileWrapper
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.contrib.staticfiles.storage import StaticFilesStorage
from django.core.exceptions import FieldError, ValidationError from django.core.exceptions import FieldError, ValidationError
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
from django.core.validators import URLValidator from django.core.validators import URLValidator
@@ -241,12 +242,22 @@ def getLogoImage(as_file=False, custom=True):
"""Return the path to the logo-file.""" """Return the path to the logo-file."""
if custom and settings.CUSTOM_LOGO: if custom and settings.CUSTOM_LOGO:
if as_file: static_storage = StaticFilesStorage()
return f"file://{default_storage.path(settings.CUSTOM_LOGO)}"
else:
return default_storage.url(settings.CUSTOM_LOGO)
if static_storage.exists(settings.CUSTOM_LOGO):
storage = static_storage
elif default_storage.exists(settings.CUSTOM_LOGO):
storage = default_storage
else: else:
storage = None
if storage is not None:
if as_file:
return f"file://{storage.path(settings.CUSTOM_LOGO)}"
else:
return storage.url(settings.CUSTOM_LOGO)
# If we have got to this point, return the default logo
if as_file: if as_file:
path = settings.STATIC_ROOT.joinpath('img/inventree.png') path = settings.STATIC_ROOT.joinpath('img/inventree.png')
return f"file://{path}" return f"file://{path}"
@@ -283,6 +294,22 @@ def str2bool(text, test=True):
return str(text).lower() in ['0', 'n', 'no', 'none', 'f', 'false', 'off', ] return str(text).lower() in ['0', 'n', 'no', 'none', 'f', 'false', 'off', ]
def str2int(text, default=None):
"""Convert a string to int if possible
Args:
text: Int like string
default: Return value if str is no int like
Returns:
Converted int value
"""
try:
return int(text)
except Exception:
return default
def is_bool(text): def is_bool(text):
"""Determine if a string value 'looks' like a boolean.""" """Determine if a string value 'looks' like a boolean."""
if str2bool(text, True): if str2bool(text, True):
+10 -6
View File
@@ -116,6 +116,12 @@ class InvenTreeMetadata(SimpleMetadata):
model_class = None model_class = None
# Attributes to copy extra attributes from the model to the field (if they don't exist)
extra_attributes = [
'help_text',
'max_length',
]
try: try:
model_class = serializer.Meta.model model_class = serializer.Meta.model
@@ -148,10 +154,7 @@ class InvenTreeMetadata(SimpleMetadata):
elif name in model_default_values: elif name in model_default_values:
serializer_info[name]['default'] = model_default_values[name] serializer_info[name]['default'] = model_default_values[name]
# Attributes to copy from the model to the field (if they don't exist) for attr in extra_attributes:
attributes = ['help_text']
for attr in attributes:
if attr not in serializer_info[name]: if attr not in serializer_info[name]:
if hasattr(field, attr): if hasattr(field, attr):
@@ -172,8 +175,9 @@ class InvenTreeMetadata(SimpleMetadata):
# This is used to automatically filter AJAX requests # This is used to automatically filter AJAX requests
serializer_info[name]['filters'] = relation.model_field.get_limit_choices_to() serializer_info[name]['filters'] = relation.model_field.get_limit_choices_to()
if 'help_text' not in serializer_info[name] and hasattr(relation.model_field, 'help_text'): for attr in extra_attributes:
serializer_info[name]['help_text'] = relation.model_field.help_text if attr not in serializer_info[name] and hasattr(relation.model_field, attr):
serializer_info[name][attr] = getattr(relation.model_field, attr)
if name in model_default_values: if name in model_default_values:
serializer_info[name]['default'] = model_default_values[name] serializer_info[name]['default'] = model_default_values[name]
+19 -5
View File
@@ -16,6 +16,7 @@ import sys
from pathlib import Path from pathlib import Path
import django.conf.locale import django.conf.locale
from django.contrib.staticfiles.storage import StaticFilesStorage
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
from django.http import Http404 from django.http import Http404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@@ -127,7 +128,7 @@ STATFILES_I18_PROCESSORS = [
] ]
# Color Themes Directory # Color Themes Directory
STATIC_COLOR_THEMES_DIR = STATIC_ROOT.joinpath('css', 'color-themes') STATIC_COLOR_THEMES_DIR = STATIC_ROOT.joinpath('css', 'color-themes').resolve()
# Web URL endpoint for served media files # Web URL endpoint for served media files
MEDIA_URL = '/media/' MEDIA_URL = '/media/'
@@ -135,6 +136,8 @@ MEDIA_URL = '/media/'
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
# Admin site integration
'django.contrib.admin',
# InvenTree apps # InvenTree apps
'build.apps.BuildConfig', 'build.apps.BuildConfig',
@@ -150,7 +153,6 @@ INSTALLED_APPS = [
'InvenTree.apps.InvenTreeConfig', # InvenTree app runs last 'InvenTree.apps.InvenTreeConfig', # InvenTree app runs last
# Core django modules # Core django modules
'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'user_sessions', # db user sessions 'user_sessions', # db user sessions
@@ -820,9 +822,21 @@ CUSTOMIZE = get_setting('INVENTREE_CUSTOMIZE', 'customize', {})
CUSTOM_LOGO = get_setting('INVENTREE_CUSTOM_LOGO', 'customize.logo', None) CUSTOM_LOGO = get_setting('INVENTREE_CUSTOM_LOGO', 'customize.logo', None)
# check that the logo-file exsists in media """
if CUSTOM_LOGO and not default_storage.exists(CUSTOM_LOGO): # pragma: no cover Check for the existence of a 'custom logo' file:
logger.warning(f"The custom logo file '{CUSTOM_LOGO}' could not be found in the default media storage") - Check the 'static' directory
- Check the 'media' directory (legacy)
"""
if CUSTOM_LOGO:
static_storage = StaticFilesStorage()
if static_storage.exists(CUSTOM_LOGO):
logger.info(f"Loading custom logo from static directory: {CUSTOM_LOGO}")
elif default_storage.exists(CUSTOM_LOGO):
logger.info(f"Loading custom logo from media directory: {CUSTOM_LOGO}")
else:
logger.warning(f"The custom logo file '{CUSTOM_LOGO}' could not be found in the static or media directories")
CUSTOM_LOGO = False CUSTOM_LOGO = False
if DEBUG: if DEBUG:
+13
View File
@@ -119,3 +119,16 @@ class ViewTests(InvenTreeTestCase):
for panel in staff_panels + plugin_panels: for panel in staff_panels + plugin_panels:
self.assertNotIn(f"select-{panel}", content) self.assertNotIn(f"select-{panel}", content)
self.assertNotIn(f"panel-{panel}", content) self.assertNotIn(f"panel-{panel}", content)
def test_url_login(self):
"""Test logging in via arguments"""
# Log out
self.client.logout()
response = self.client.get("/index/")
self.assertEqual(response.status_code, 302)
# Try login with url
response = self.client.get(f"/accounts/login/?next=/&login={self.username}&password={self.password}")
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, '/')
+4 -1
View File
@@ -32,7 +32,7 @@ from users.api import user_urls
from .api import InfoView, NotFoundView from .api import InfoView, NotFoundView
from .views import (AboutView, AppearanceSelectView, CurrencyRefreshView, from .views import (AboutView, AppearanceSelectView, CurrencyRefreshView,
CustomConnectionsView, CustomEmailView, CustomConnectionsView, CustomEmailView, CustomLoginView,
CustomPasswordResetFromKeyView, CustomPasswordResetFromKeyView,
CustomSessionDeleteOtherView, CustomSessionDeleteView, CustomSessionDeleteOtherView, CustomSessionDeleteView,
CustomTwoFactorRemove, DatabaseStatsView, DynamicJsView, CustomTwoFactorRemove, DatabaseStatsView, DynamicJsView,
@@ -168,6 +168,9 @@ frontendpatterns = [
# See https://github.com/inventree/InvenTree/security/advisories/GHSA-8j76-mm54-52xq # See https://github.com/inventree/InvenTree/security/advisories/GHSA-8j76-mm54-52xq
re_path(r'^accounts/two_factor/remove/?$', CustomTwoFactorRemove.as_view(), name='two-factor-remove'), re_path(r'^accounts/two_factor/remove/?$', CustomTwoFactorRemove.as_view(), name='two-factor-remove'),
# Override login page
re_path("accounts/login/", CustomLoginView.as_view(), name="account_login"),
re_path(r'^accounts/', include('allauth_2fa.urls')), # MFA support re_path(r'^accounts/', include('allauth_2fa.urls')), # MFA support
re_path(r'^accounts/', include('allauth.urls')), # included urlpatterns re_path(r'^accounts/', include('allauth.urls')), # included urlpatterns
] ]
+19 -1
View File
@@ -24,7 +24,8 @@ from django.views.generic.base import RedirectView, TemplateView
from allauth.account.forms import AddEmailForm from allauth.account.forms import AddEmailForm
from allauth.account.models import EmailAddress from allauth.account.models import EmailAddress
from allauth.account.views import EmailView, PasswordResetFromKeyView from allauth.account.views import (EmailView, LoginView,
PasswordResetFromKeyView)
from allauth.socialaccount.forms import DisconnectForm from allauth.socialaccount.forms import DisconnectForm
from allauth.socialaccount.views import ConnectionsView from allauth.socialaccount.views import ConnectionsView
from allauth_2fa.views import TwoFactorRemove from allauth_2fa.views import TwoFactorRemove
@@ -700,6 +701,23 @@ class CustomSessionDeleteOtherView(UserSessionOverride, SessionDeleteOtherView):
pass pass
class CustomLoginView(LoginView):
"""Custom login view that allows login with urlargs."""
def get(self, request, *args, **kwargs):
"""Extendend get to allow for auth via url args."""
# Check if login is present
if 'login' in request.GET:
# Initiate form
form = self.get_form_class()(request.GET.dict(), request=request)
# Try to login
form.full_clean()
return form.login(request)
return super().get(request, *args, **kwargs)
class CurrencyRefreshView(RedirectView): class CurrencyRefreshView(RedirectView):
"""POST endpoint to refresh / update exchange rates.""" """POST endpoint to refresh / update exchange rates."""
+14 -4
View File
@@ -10,7 +10,6 @@ import hmac
import json import json
import logging import logging
import math import math
import os
import uuid import uuid
from datetime import datetime, timedelta from datetime import datetime, timedelta
from enum import Enum from enum import Enum
@@ -877,6 +876,16 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'default': True, 'default': True,
}, },
'INVENTREE_TREE_DEPTH': {
'name': _('Tree Depth'),
'description': _('Default tree depth for treeview. Deeper levels can be lazy loaded as they are needed.'),
'default': 1,
'validator': [
int,
MinValueValidator(0),
]
},
'BARCODE_ENABLE': { 'BARCODE_ENABLE': {
'name': _('Barcode Support'), 'name': _('Barcode Support'),
'description': _('Enable barcode scanner support'), 'description': _('Enable barcode scanner support'),
@@ -1775,14 +1784,15 @@ class ColorTheme(models.Model):
@classmethod @classmethod
def get_color_themes_choices(cls): def get_color_themes_choices(cls):
"""Get all color themes from static folder.""" """Get all color themes from static folder."""
if settings.TESTING and not os.path.exists(settings.STATIC_COLOR_THEMES_DIR): if not settings.STATIC_COLOR_THEMES_DIR.exists():
logger.error('Theme directory does not exsist') logger.error('Theme directory does not exsist')
return [] return []
# Get files list from css/color-themes/ folder # Get files list from css/color-themes/ folder
files_list = [] files_list = []
for file in os.listdir(settings.STATIC_COLOR_THEMES_DIR):
files_list.append(os.path.splitext(file)) for file in settings.STATIC_COLOR_THEMES_DIR.iterdir():
files_list.append([file.stem, file.suffix])
# Get color themes choices (CSS sheets) # Get color themes choices (CSS sheets)
choices = [(file_name.lower(), _(file_name.replace('-', ' ').title())) choices = [(file_name.lower(), _(file_name.replace('-', ' ').title()))
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
+1 -1
View File
@@ -1376,7 +1376,7 @@ class SalesOrderAllocation(models.Model):
# TODO: The logic here needs improving. Do we need to subtract our own amount, or something? # TODO: The logic here needs improving. Do we need to subtract our own amount, or something?
if self.item.quantity - self.item.allocation_count() + self.quantity < self.quantity: if self.item.quantity - self.item.allocation_count() + self.quantity < self.quantity:
errors['quantity'] = _('StockItem is over-allocated') errors['quantity'] = _('Stock item is over-allocated')
if self.quantity <= 0: if self.quantity <= 0:
errors['quantity'] = _('Allocation quantity must be greater than zero') errors['quantity'] = _('Allocation quantity must be greater than zero')
+10 -1
View File
@@ -25,7 +25,8 @@ from company.models import Company, ManufacturerPart, SupplierPart
from InvenTree.api import (APIDownloadMixin, AttachmentMixin, from InvenTree.api import (APIDownloadMixin, AttachmentMixin,
ListCreateDestroyAPIView) ListCreateDestroyAPIView)
from InvenTree.filters import InvenTreeOrderingFilter from InvenTree.filters import InvenTreeOrderingFilter
from InvenTree.helpers import DownloadFile, increment, isNull, str2bool from InvenTree.helpers import (DownloadFile, increment, isNull, str2bool,
str2int)
from InvenTree.mixins import (CreateAPI, ListAPI, ListCreateAPI, RetrieveAPI, from InvenTree.mixins import (CreateAPI, ListAPI, ListCreateAPI, RetrieveAPI,
RetrieveUpdateAPI, RetrieveUpdateDestroyAPI, RetrieveUpdateAPI, RetrieveUpdateDestroyAPI,
UpdateAPI) UpdateAPI)
@@ -85,6 +86,8 @@ class CategoryList(ListCreateAPI):
cascade = str2bool(params.get('cascade', False)) cascade = str2bool(params.get('cascade', False))
depth = str2int(params.get('depth', None))
# Do not filter by category # Do not filter by category
if cat_id is None: if cat_id is None:
pass pass
@@ -94,12 +97,18 @@ class CategoryList(ListCreateAPI):
if not cascade: if not cascade:
queryset = queryset.filter(parent=None) queryset = queryset.filter(parent=None)
if cascade and depth is not None:
queryset = queryset.filter(level__lte=depth)
else: else:
try: try:
category = PartCategory.objects.get(pk=cat_id) category = PartCategory.objects.get(pk=cat_id)
if cascade: if cascade:
parents = category.get_descendants(include_self=True) parents = category.get_descendants(include_self=True)
if depth is not None:
parents = parents.filter(level__lte=category.level + depth)
parent_ids = [p.id for p in parents] parent_ids = [p.id for p in parents]
queryset = queryset.filter(parent__in=parent_ids) queryset = queryset.filter(parent__in=parent_ids)
+30 -26
View File
@@ -49,33 +49,36 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
"""Test the PartCategoryList API endpoint""" """Test the PartCategoryList API endpoint"""
url = reverse('api-part-category-list') url = reverse('api-part-category-list')
response = self.get(url, expected_code=200) # star categories manually for tests as it is not possible with fixures
# because the current user is no fixure itself and throws an invalid
# foreign key constrain
for pk in [3, 4]:
PartCategory.objects.get(pk=pk).set_starred(self.user, True)
self.assertEqual(len(response.data), 8) test_cases = [
({}, 8, 'no parameters'),
({'parent': 1, 'cascade': False}, 3, 'Filter by parent, no cascading'),
({'parent': 1, 'cascade': True}, 5, 'Filter by parent, cascading'),
({'cascade': True, 'depth': 0}, 8, 'Cascade with no parent, depth=0'),
({'cascade': False, 'depth': 10}, 8, 'Cascade with no parent, depth=0'),
({'parent': 'null', 'cascade': True, 'depth': 0}, 2, 'Cascade with null parent, depth=0'),
({'parent': 'null', 'cascade': True, 'depth': 10}, 8, 'Cascade with null parent and bigger depth'),
({'parent': 'null', 'cascade': False, 'depth': 10}, 2, 'No cascade even with depth specified with null parent'),
({'parent': 1, 'cascade': False, 'depth': 0}, 3, 'Dont cascade with depth=0 and parent'),
({'parent': 1, 'cascade': True, 'depth': 0}, 3, 'Cascade with depth=0 and parent'),
({'parent': 1, 'cascade': False, 'depth': 1}, 3, 'Dont cascade even with depth=1 specified with parent'),
({'parent': 1, 'cascade': True, 'depth': 1}, 5, 'Cascade with depth=1 with parent'),
({'parent': 1, 'cascade': True, 'depth': 'abcdefg'}, 5, 'Cascade with invalid depth and parent'),
({'parent': 42}, 8, 'Should return everything if parent_pk is not vaild'),
({'parent': 'null', 'exclude_tree': 1, 'cascade': True}, 2, 'Should return everything from except tree with pk=1'),
({'parent': 'null', 'exclude_tree': 42, 'cascade': True}, 8, 'Should return everything because exclude_tree=42 is no valid pk'),
({'parent': 1, 'starred': True, 'cascade': True}, 2, 'Should return the starred categories for the current user within the pk=1 tree'),
({'parent': 1, 'starred': False, 'cascade': True}, 3, 'Should return the not starred categories for the current user within the pk=1 tree'),
]
# Filter by parent, depth=1 for params, res_len, description in test_cases:
response = self.get( response = self.get(url, params, expected_code=200)
url, self.assertEqual(len(response.data), res_len, description)
{
'parent': 1,
'cascade': False,
},
expected_code=200
)
self.assertEqual(len(response.data), 3)
# Filter by parent, cascading
response = self.get(
url,
{
'parent': 1,
'cascade': True,
},
expected_code=200,
)
self.assertEqual(len(response.data), 5)
# Check that the required fields are present # Check that the required fields are present
fields = [ fields = [
@@ -90,9 +93,10 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
'url' 'url'
] ]
response = self.get(url, expected_code=200)
for result in response.data: for result in response.data:
for f in fields: for f in fields:
self.assertIn(f, result) self.assertIn(f, result, f'"{f}" is missing in result of PartCategory list')
def test_part_count(self): def test_part_count(self):
"""Test that the 'part_count' field is annotated correctly""" """Test that the 'part_count' field is annotated correctly"""
+9 -1
View File
@@ -26,7 +26,7 @@ from InvenTree.api import (APIDownloadMixin, AttachmentMixin,
ListCreateDestroyAPIView) ListCreateDestroyAPIView)
from InvenTree.filters import InvenTreeOrderingFilter from InvenTree.filters import InvenTreeOrderingFilter
from InvenTree.helpers import (DownloadFile, extract_serial_numbers, isNull, from InvenTree.helpers import (DownloadFile, extract_serial_numbers, isNull,
str2bool) str2bool, str2int)
from InvenTree.mixins import (CreateAPI, ListAPI, ListCreateAPI, RetrieveAPI, from InvenTree.mixins import (CreateAPI, ListAPI, ListCreateAPI, RetrieveAPI,
RetrieveUpdateAPI, RetrieveUpdateDestroyAPI) RetrieveUpdateAPI, RetrieveUpdateDestroyAPI)
from order.models import PurchaseOrder, SalesOrder, SalesOrderAllocation from order.models import PurchaseOrder, SalesOrder, SalesOrderAllocation
@@ -241,6 +241,8 @@ class StockLocationList(ListCreateAPI):
cascade = str2bool(params.get('cascade', False)) cascade = str2bool(params.get('cascade', False))
depth = str2int(params.get('depth', None))
# Do not filter by location # Do not filter by location
if loc_id is None: if loc_id is None:
pass pass
@@ -251,6 +253,9 @@ class StockLocationList(ListCreateAPI):
if not cascade: if not cascade:
queryset = queryset.filter(parent=None) queryset = queryset.filter(parent=None)
if cascade and depth is not None:
queryset = queryset.filter(level__lte=depth)
else: else:
try: try:
@@ -259,6 +264,9 @@ class StockLocationList(ListCreateAPI):
# All sub-locations to be returned too? # All sub-locations to be returned too?
if cascade: if cascade:
parents = location.get_descendants(include_self=True) parents = location.get_descendants(include_self=True)
if depth is not None:
parents = parents.filter(level__lte=location.level + depth)
parent_ids = [p.id for p in parents] parent_ids = [p.id for p in parents]
queryset = queryset.filter(parent__in=parent_ids) queryset = queryset.filter(parent__in=parent_ids)
@@ -0,0 +1,19 @@
# Generated by Django 3.2.15 on 2022-08-07 02:38
import InvenTree.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('stock', '0081_auto_20220801_0044'),
]
operations = [
migrations.AlterField(
model_name='stockitem',
name='link',
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', max_length=200, verbose_name='External Link'),
),
]
+1 -1
View File
@@ -647,7 +647,7 @@ class StockItem(MetadataMixin, MPTTModel):
link = InvenTreeURLField( link = InvenTreeURLField(
verbose_name=_('External Link'), verbose_name=_('External Link'),
max_length=125, blank=True, blank=True, max_length=200,
help_text=_("Link to external URL") help_text=_("Link to external URL")
) )
+41 -5
View File
@@ -54,11 +54,47 @@ class StockLocationTest(StockAPITestCase):
StockLocation.objects.create(name='top', description='top category') StockLocation.objects.create(name='top', description='top category')
def test_list(self): def test_list(self):
"""Test StockLocation list.""" """Test the StockLocationList API endpoint"""
# Check that we can request the StockLocation list test_cases = [
response = self.client.get(self.list_url, format='json') ({}, 8, 'no parameters'),
self.assertEqual(response.status_code, status.HTTP_200_OK) ({'parent': 1, 'cascade': False}, 2, 'Filter by parent, no cascading'),
self.assertGreaterEqual(len(response.data), 1) ({'parent': 1, 'cascade': True}, 2, 'Filter by parent, cascading'),
({'cascade': True, 'depth': 0}, 8, 'Cascade with no parent, depth=0'),
({'cascade': False, 'depth': 10}, 8, 'Cascade with no parent, depth=0'),
({'parent': 'null', 'cascade': True, 'depth': 0}, 7, 'Cascade with null parent, depth=0'),
({'parent': 'null', 'cascade': True, 'depth': 10}, 8, 'Cascade with null parent and bigger depth'),
({'parent': 'null', 'cascade': False, 'depth': 10}, 3, 'No cascade even with depth specified with null parent'),
({'parent': 1, 'cascade': False, 'depth': 0}, 2, 'Dont cascade with depth=0 and parent'),
({'parent': 1, 'cascade': True, 'depth': 0}, 2, 'Cascade with depth=0 and parent'),
({'parent': 1, 'cascade': False, 'depth': 1}, 2, 'Dont cascade even with depth=1 specified with parent'),
({'parent': 1, 'cascade': True, 'depth': 1}, 2, 'Cascade with depth=1 with parent'),
({'parent': 1, 'cascade': True, 'depth': 'abcdefg'}, 2, 'Cascade with invalid depth and parent'),
({'parent': 42}, 8, 'Should return everything if parent_pk is not vaild'),
({'parent': 'null', 'exclude_tree': 1, 'cascade': True}, 5, 'Should return everything except tree with pk=1'),
({'parent': 'null', 'exclude_tree': 42, 'cascade': True}, 8, 'Should return everything because exclude_tree=42 is no valid pk'),
]
for params, res_len, description in test_cases:
response = self.get(self.list_url, params, expected_code=200)
self.assertEqual(len(response.data), res_len, description)
# Check that the required fields are present
fields = [
'pk',
'name',
'description',
'level',
'parent',
'items',
'pathstring',
'owner',
'url'
]
response = self.get(self.list_url, expected_code=200)
for result in response.data:
for f in fields:
self.assertIn(f, result, f'"{f}" is missing in result of StockLocation list')
def test_add(self): def test_add(self):
"""Test adding StockLocation.""" """Test adding StockLocation."""
+39
View File
@@ -44,6 +44,45 @@ class StockTest(InvenTreeTestCase):
Part.objects.rebuild() Part.objects.rebuild()
StockItem.objects.rebuild() StockItem.objects.rebuild()
def test_link(self):
"""Test the link URL field validation"""
item = StockItem.objects.get(pk=1)
# Check that invalid URLs fail
for bad_url in [
'test.com',
'httpx://abc.xyz',
'https:google.com',
]:
with self.assertRaises(ValidationError):
item.link = bad_url
item.save()
item.full_clean()
# Check that valid URLs pass
for good_url in [
'https://test.com',
'https://digikey.com/datasheets?file=1010101010101.bin',
'ftp://download.com:8080/file.aspx',
]:
item.link = good_url
item.save()
item.full_clean()
# A long URL should fail
long_url = 'https://website.co.uk?query=' + 'a' * 173
with self.assertRaises(ValidationError):
item.link = long_url
item.full_clean()
# Shorten by a single character, will pass
long_url = long_url[:-1]
item.link = long_url
item.save()
def test_expiry(self): def test_expiry(self):
"""Test expiry date functionality for StockItem model.""" """Test expiry date functionality for StockItem model."""
today = datetime.datetime.now().date() today = datetime.datetime.now().date()
@@ -22,6 +22,7 @@
{% include "InvenTree/settings/setting.html" with key="INVENTREE_DOWNLOAD_FROM_URL" icon="fa-cloud-download-alt" %} {% include "InvenTree/settings/setting.html" with key="INVENTREE_DOWNLOAD_FROM_URL" icon="fa-cloud-download-alt" %}
{% include "InvenTree/settings/setting.html" with key="INVENTREE_DOWNLOAD_IMAGE_MAX_SIZE" icon="fa-server" %} {% include "InvenTree/settings/setting.html" with key="INVENTREE_DOWNLOAD_IMAGE_MAX_SIZE" icon="fa-server" %}
{% include "InvenTree/settings/setting.html" with key="INVENTREE_REQUIRE_CONFIRM" icon="fa-check" %} {% include "InvenTree/settings/setting.html" with key="INVENTREE_REQUIRE_CONFIRM" icon="fa-check" %}
{% include "InvenTree/settings/setting.html" with key="INVENTREE_TREE_DEPTH" icon="fa-sitemap" %}
</tbody> </tbody>
</table> </table>
+57 -1
View File
@@ -1750,6 +1750,7 @@ function loadPartCategoryTable(table, options) {
if (tree_view) { if (tree_view) {
params.cascade = true; params.cascade = true;
params.depth = global_settings.INVENTREE_TREE_DEPTH;
} }
var original = {}; var original = {};
@@ -1761,6 +1762,35 @@ function loadPartCategoryTable(table, options) {
setupFilterList(filterKey, table, filterListElement); setupFilterList(filterKey, table, filterListElement);
// Function to request sub-category items
function requestSubItems(parent_pk) {
inventreeGet(
options.url || '{% url "api-part-category-list" %}',
{
parent: parent_pk,
},
{
success: function(response) {
// Add the returned sub-items to the table
for (var idx = 0; idx < response.length; idx++) {
response[idx].parent = parent_pk;
}
const row = $(table).bootstrapTable('getRowByUniqueId', parent_pk);
row.subReceived = true;
$(table).bootstrapTable('updateByUniqueId', parent_pk, row, true);
table.bootstrapTable('append', response);
},
error: function(xhr) {
console.error('Error requesting sub-category for category=' + parent_pk);
showApiError(xhr);
}
}
);
}
table.inventreeTable({ table.inventreeTable({
treeEnable: tree_view, treeEnable: tree_view,
rootParentId: tree_view ? options.params.parent : null, rootParentId: tree_view ? options.params.parent : null,
@@ -1839,6 +1869,20 @@ function loadPartCategoryTable(table, options) {
} }
}); });
// Callback for 'load sub category' button
$(table).find('.load-sub-category').click(function(event) {
event.preventDefault();
const pk = $(this).attr('pk');
const row = $(table).bootstrapTable('getRowByUniqueId', pk);
// Request sub-category for this category
requestSubItems(row.pk);
row.subRequested = true;
$(table).bootstrapTable('updateByUniqueId', pk, row, true);
});
} else { } else {
$('#view-category-tree').removeClass('btn-secondary').addClass('btn-outline-secondary'); $('#view-category-tree').removeClass('btn-secondary').addClass('btn-outline-secondary');
$('#view-category-list').removeClass('btn-outline-secondary').addClass('btn-secondary'); $('#view-category-list').removeClass('btn-outline-secondary').addClass('btn-secondary');
@@ -1859,8 +1903,20 @@ function loadPartCategoryTable(table, options) {
switchable: true, switchable: true,
sortable: true, sortable: true,
formatter: function(value, row) { formatter: function(value, row) {
let html = '';
var html = renderLink( if (row._level >= global_settings.INVENTREE_TREE_DEPTH && !row.subReceived) {
if (row.subRequested) {
html += `<a href='#'><span class='fas fa-sync fa-spin'></span></a>`;
} else {
html += `
<a href='#' pk='${row.pk}' class='load-sub-category'>
<span class='fas fa-sync-alt' title='{% trans "Load Subcategories" %}'></span>
</a> `;
}
}
html += renderLink(
value, value,
`/part/category/${row.pk}/` `/part/category/${row.pk}/`
); );
+60 -1
View File
@@ -2226,6 +2226,7 @@ function loadStockLocationTable(table, options) {
if (tree_view) { if (tree_view) {
params.cascade = true; params.cascade = true;
params.depth = global_settings.INVENTREE_TREE_DEPTH;
} }
var filters = {}; var filters = {};
@@ -2248,6 +2249,35 @@ function loadStockLocationTable(table, options) {
filters[key] = params[key]; filters[key] = params[key];
} }
// Function to request sub-location items
function requestSubItems(parent_pk) {
inventreeGet(
options.url || '{% url "api-location-list" %}',
{
parent: parent_pk,
},
{
success: function(response) {
// Add the returned sub-items to the table
for (var idx = 0; idx < response.length; idx++) {
response[idx].parent = parent_pk;
}
const row = $(table).bootstrapTable('getRowByUniqueId', parent_pk);
row.subReceived = true;
$(table).bootstrapTable('updateByUniqueId', parent_pk, row, true);
table.bootstrapTable('append', response);
},
error: function(xhr) {
console.error('Error requesting sub-locations for location=' + parent_pk);
showApiError(xhr);
}
}
);
}
table.inventreeTable({ table.inventreeTable({
treeEnable: tree_view, treeEnable: tree_view,
rootParentId: tree_view ? options.params.parent : null, rootParentId: tree_view ? options.params.parent : null,
@@ -2286,6 +2316,20 @@ function loadStockLocationTable(table, options) {
} }
}); });
// Callback for 'load sub location' button
$(table).find('.load-sub-location').click(function(event) {
event.preventDefault();
const pk = $(this).attr('pk');
const row = $(table).bootstrapTable('getRowByUniqueId', pk);
// Request sub-location for this location
requestSubItems(row.pk);
row.subRequested = true;
$(table).bootstrapTable('updateByUniqueId', pk, row, true);
});
} else { } else {
$('#view-location-tree').removeClass('btn-secondary').addClass('btn-outline-secondary'); $('#view-location-tree').removeClass('btn-secondary').addClass('btn-outline-secondary');
$('#view-location-list').removeClass('btn-outline-secondary').addClass('btn-secondary'); $('#view-location-list').removeClass('btn-outline-secondary').addClass('btn-secondary');
@@ -2345,10 +2389,25 @@ function loadStockLocationTable(table, options) {
switchable: true, switchable: true,
sortable: true, sortable: true,
formatter: function(value, row) { formatter: function(value, row) {
return renderLink( let html = '';
if (row._level >= global_settings.INVENTREE_TREE_DEPTH && !row.subReceived) {
if (row.subRequested) {
html += `<a href='#'><span class='fas fa-sync fa-spin'></span></a>`;
} else {
html += `
<a href='#' pk='${row.pk}' class='load-sub-location'>
<span class='fas fa-sync-alt' title='{% trans "Load Subloactions" %}'></span>
</a> `;
}
}
html += renderLink(
value, value,
`/stock/location/${row.pk}/` `/stock/location/${row.pk}/`
); );
return html;
}, },
}, },
{ {
+21 -2
View File
@@ -87,8 +87,16 @@ if __name__ == '__main__':
# GITHUB_REF may be either 'refs/heads/<branch>' or 'refs/heads/<tag>' # GITHUB_REF may be either 'refs/heads/<branch>' or 'refs/heads/<tag>'
GITHUB_REF = os.environ['GITHUB_REF'] GITHUB_REF = os.environ['GITHUB_REF']
GITHUB_REF_NAME = os.environ['GITHUB_REF_NAME']
GITHUB_BASE_REF = os.environ['GITHUB_BASE_REF'] GITHUB_BASE_REF = os.environ['GITHUB_BASE_REF']
# Print out version information, makes debugging actions *much* easier!
print(f"GITHUB_REF: {GITHUB_REF}")
print(f"GITHUB_REF_NAME: {GITHUB_REF_NAME}")
print(f"GITHUB_REF_TYPE: {GITHUB_REF_TYPE}")
print(f"GITHUB_BASE_REF: {GITHUB_BASE_REF}")
version_file = os.path.join(here, '..', 'InvenTree', 'InvenTree', 'version.py') version_file = os.path.join(here, '..', 'InvenTree', 'InvenTree', 'version.py')
version = None version = None
@@ -109,8 +117,19 @@ if __name__ == '__main__':
print(f"InvenTree Version: '{version}'") print(f"InvenTree Version: '{version}'")
# Check version number and look for existing versions # Check version number and look for existing versions
# Note that on a 'tag' (release) we *must* allow duplicate versions, as this *is* the version that has just been released # If a release is found which matches the current tag, throw an error
highest_release = check_version_number(version, allow_duplicate=GITHUB_REF_TYPE == 'tag')
allow_duplicate = False
# Note: on a 'tag' (release) we *must* allow duplicate versions, as this *is* the version that has just been released
if GITHUB_REF_TYPE == 'tag':
allow_duplicate = True
# Note: on a push to 'stable' branch we also allow duplicates
if GITHUB_BASE_REF == 'stable':
allow_duplicate = True
highest_release = check_version_number(version, allow_duplicate=allow_duplicate)
# Determine which docker tag we are going to use # Determine which docker tag we are going to use
docker_tags = None docker_tags = None
+64 -56
View File
@@ -10,6 +10,10 @@ asgiref==3.5.2 \
# via # via
# -c requirements.txt # -c requirements.txt
# django # django
build==0.8.0 \
--hash=sha256:19b0ed489f92ace6947698c3ca8436cb0556a66e2aa2d34cd70e2a5d27cd0437 \
--hash=sha256:887a6d471c901b1a6e6574ebaeeebb45e5269a79d095fe9a8f88d6614ed2e5f0
# via pip-tools
certifi==2022.6.15 \ certifi==2022.6.15 \
--hash=sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d \ --hash=sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d \
--hash=sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412 --hash=sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412
@@ -20,9 +24,9 @@ cfgv==3.3.1 \
--hash=sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426 \ --hash=sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426 \
--hash=sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736 --hash=sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736
# via pre-commit # via pre-commit
charset-normalizer==2.0.12 \ charset-normalizer==2.1.0 \
--hash=sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597 \ --hash=sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5 \
--hash=sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df --hash=sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413
# via # via
# -c requirements.txt # -c requirements.txt
# requests # requests
@@ -90,13 +94,13 @@ coveralls==2.1.2 \
--hash=sha256:4430b862baabb3cf090d36d84d331966615e4288d8a8c5957e0fd456d0dd8bd6 \ --hash=sha256:4430b862baabb3cf090d36d84d331966615e4288d8a8c5957e0fd456d0dd8bd6 \
--hash=sha256:b3b60c17b03a0dee61952a91aed6f131e0b2ac8bd5da909389c53137811409e1 --hash=sha256:b3b60c17b03a0dee61952a91aed6f131e0b2ac8bd5da909389c53137811409e1
# via -r requirements-dev.in # via -r requirements-dev.in
distlib==0.3.4 \ distlib==0.3.5 \
--hash=sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b \ --hash=sha256:a7f75737c70be3b25e2bee06288cec4e4c221de18455b2dd037fe2a795cab2fe \
--hash=sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579 --hash=sha256:b710088c59f06338ca514800ad795a132da19fda270e3ce4affc74abf955a26c
# via virtualenv # via virtualenv
django==3.2.14 \ django==3.2.15 \
--hash=sha256:677182ba8b5b285a4e072f3ac17ceee6aff1b5ce77fd173cc5b6a2d3dc022fcf \ --hash=sha256:115baf5049d5cf4163e43492cdc7139c306ed6d451e7d3571fe9612903903713 \
--hash=sha256:a8681e098fa60f7c33a4b628d6fcd3fe983a0939ff1301ecacac21d0b38bad56 --hash=sha256:f71934b1a822f14a86c9ac9634053689279cd04ae69cb6ade4a59471b886582b
# via # via
# -c requirements.txt # -c requirements.txt
# django-debug-toolbar # django-debug-toolbar
@@ -115,9 +119,9 @@ filelock==3.7.1 \
--hash=sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404 \ --hash=sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404 \
--hash=sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04 --hash=sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04
# via virtualenv # via virtualenv
flake8==4.0.1 \ flake8==5.0.4 \
--hash=sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d \ --hash=sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db \
--hash=sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d --hash=sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248
# via # via
# -r requirements-dev.in # -r requirements-dev.in
# flake8-docstrings # flake8-docstrings
@@ -126,9 +130,9 @@ flake8-docstrings==1.6.0 \
--hash=sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde \ --hash=sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde \
--hash=sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b --hash=sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b
# via -r requirements-dev.in # via -r requirements-dev.in
identify==2.5.1 \ identify==2.5.3 \
--hash=sha256:0dca2ea3e4381c435ef9c33ba100a78a9b40c0bab11189c7cf121f75815efeaa \ --hash=sha256:25851c8c1370effb22aaa3c987b30449e9ff0cece408f810ae6ce408fdd20893 \
--hash=sha256:3d11b16f3fe19f52039fb7e39c9c884b21cb1b586988114fbe42671f03de3e82 --hash=sha256:887e7b91a1be152b0d46bbf072130235a8117392b9f1828446079a816a05ef44
# via pre-commit # via pre-commit
idna==3.3 \ idna==3.3 \
--hash=sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff \ --hash=sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff \
@@ -140,46 +144,54 @@ isort==5.10.1 \
--hash=sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7 \ --hash=sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7 \
--hash=sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951 --hash=sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951
# via -r requirements-dev.in # via -r requirements-dev.in
mccabe==0.6.1 \ mccabe==0.7.0 \
--hash=sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42 \ --hash=sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325 \
--hash=sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f --hash=sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e
# via flake8 # via flake8
nodeenv==1.7.0 \ nodeenv==1.7.0 \
--hash=sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e \ --hash=sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e \
--hash=sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b --hash=sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b
# via pre-commit # via pre-commit
pep517==0.12.0 \ packaging==21.3 \
--hash=sha256:931378d93d11b298cf511dd634cf5ea4cb249a28ef84160b3247ee9afb4e8ab0 \ --hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \
--hash=sha256:dd884c326898e2c6e11f9e0b64940606a93eb10ea022a2e067959f3a110cf161 --hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522
# via pip-tools # via build
pep8-naming==0.13.0 \ pep517==0.13.0 \
--hash=sha256:069ea20e97f073b3e6d4f789af2a57816f281ca64b86210c7d471117a4b6bfd0 \ --hash=sha256:4ba4446d80aed5b5eac6509ade100bff3e7943a8489de249654a5ae9b33ee35b \
--hash=sha256:9f38e6dcf867a1fb7ad47f5ff72c0ddae544a6cf64eb9f7600b7b3c0bb5980b5 --hash=sha256:ae69927c5c172be1add9203726d4b84cf3ebad1edcd5f71fcdc746e66e829f59
# via build
pep8-naming==0.13.1 \
--hash=sha256:3af77cdaa9c7965f7c85a56cd579354553c9bbd3fdf3078a776f12db54dd6944 \
--hash=sha256:f7867c1a464fe769be4f972ef7b79d6df1d9aff1b1f04ecf738d471963d3ab9c
# via -r requirements-dev.in # via -r requirements-dev.in
pip-tools==6.6.2 \ pip-tools==6.8.0 \
--hash=sha256:6b486548e5a139e30e4c4a225b3b7c2d46942a9f6d1a91143c21b1de4d02fd9b \ --hash=sha256:39e8aee465446e02278d80dbebd4325d1dd8633248f43213c73a25f58e7d8a55 \
--hash=sha256:f638503a9f77d98d9a7d72584b1508d3f82ed019b8fab24f4e5ad078c1b8c95e --hash=sha256:3e5cd4acbf383d19bdfdeab04738b6313ebf4ad22ce49bf529c729061eabfab8
# via -r requirements-dev.in # via -r requirements-dev.in
platformdirs==2.5.2 \ platformdirs==2.5.2 \
--hash=sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788 \ --hash=sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788 \
--hash=sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19 --hash=sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19
# via virtualenv # via virtualenv
pre-commit==2.19.0 \ pre-commit==2.20.0 \
--hash=sha256:10c62741aa5704faea2ad69cb550ca78082efe5697d6f04e5710c3c229afdd10 \ --hash=sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7 \
--hash=sha256:4233a1e38621c87d9dda9808c6606d7e7ba0e087cd56d3fe03202a01d2919615 --hash=sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959
# via -r requirements-dev.in # via -r requirements-dev.in
pycodestyle==2.8.0 \ pycodestyle==2.9.1 \
--hash=sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20 \ --hash=sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785 \
--hash=sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f --hash=sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b
# via flake8 # via flake8
pydocstyle==6.1.1 \ pydocstyle==6.1.1 \
--hash=sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc \ --hash=sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc \
--hash=sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4 --hash=sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4
# via flake8-docstrings # via flake8-docstrings
pyflakes==2.4.0 \ pyflakes==2.5.0 \
--hash=sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c \ --hash=sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2 \
--hash=sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e --hash=sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3
# via flake8 # via flake8
pyparsing==3.0.9 \
--hash=sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb \
--hash=sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc
# via packaging
pytz==2022.1 \ pytz==2022.1 \
--hash=sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7 \ --hash=sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7 \
--hash=sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c --hash=sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c
@@ -223,18 +235,12 @@ pyyaml==6.0 \
# via # via
# -c requirements.txt # -c requirements.txt
# pre-commit # pre-commit
requests==2.28.0 \ requests==2.28.1 \
--hash=sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f \ --hash=sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983 \
--hash=sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b --hash=sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349
# via # via
# -c requirements.txt # -c requirements.txt
# coveralls # coveralls
six==1.16.0 \
--hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
--hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
# via
# -c requirements.txt
# virtualenv
snowballstemmer==2.2.0 \ snowballstemmer==2.2.0 \
--hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \ --hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \
--hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a --hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a
@@ -253,20 +259,22 @@ toml==0.10.2 \
tomli==2.0.1 \ tomli==2.0.1 \
--hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
--hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
# via pep517 # via
typing-extensions==4.2.0 \ # build
--hash=sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708 \ # pep517
--hash=sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376 typing-extensions==4.3.0 \
--hash=sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02 \
--hash=sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6
# via django-test-migrations # via django-test-migrations
urllib3==1.26.9 \ urllib3==1.26.11 \
--hash=sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14 \ --hash=sha256:c33ccba33c819596124764c23a97d25f32b28433ba0dedeb77d873a38722c9bc \
--hash=sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e --hash=sha256:ea6e8fb210b19d950fab93b60c9009226c63a28808bc8386e05301e25883ac0a
# via # via
# -c requirements.txt # -c requirements.txt
# requests # requests
virtualenv==20.15.0 \ virtualenv==20.16.3 \
--hash=sha256:4c44b1d77ca81f8368e2d7414f9b20c428ad16b343ac6d226206c5b84e2b4fcc \ --hash=sha256:4193b7bc8a6cd23e4eb251ac64f29b4398ab2c233531e66e40b19a6b7b0d30c1 \
--hash=sha256:804cce4de5b8a322f099897e308eecc8f6e2951f1a8e7e2b3598dff865f01336 --hash=sha256:d86ea0bb50e06252d79e6c241507cb904fcd66090c3271381372d6221a3970f9
# via pre-commit # via pre-commit
wheel==0.37.1 \ wheel==0.37.1 \
--hash=sha256:4bdcd7d840138086126cd09254dc6195fb4fc6f01c050a1d7236f2630db1d22a \ --hash=sha256:4bdcd7d840138086126cd09254dc6195fb4fc6f01c050a1d7236f2630db1d22a \
+12 -12
View File
@@ -10,7 +10,7 @@ asgiref==3.5.2
# via django # via django
babel==2.10.3 babel==2.10.3
# via py-moneyed # via py-moneyed
bleach[css]==5.0.0 bleach[css]==5.0.1
# via django-markdownify # via django-markdownify
blessed==1.19.1 blessed==1.19.1
# via django-q # via django-q
@@ -20,11 +20,11 @@ certifi==2022.6.15
# via # via
# requests # requests
# sentry-sdk # sentry-sdk
cffi==1.15.0 cffi==1.15.1
# via # via
# cryptography # cryptography
# weasyprint # weasyprint
charset-normalizer==2.0.12 charset-normalizer==2.1.0
# via requests # via requests
coreapi==2.3.3 coreapi==2.3.3
# via -r requirements.in # via -r requirements.in
@@ -42,7 +42,7 @@ defusedxml==0.7.1
# python3-openid # python3-openid
diff-match-patch==20200713 diff-match-patch==20200713
# via django-import-export # via django-import-export
django==3.2.14 django==3.2.15
# via # via
# -r requirements.in # -r requirements.in
# django-allauth # django-allauth
@@ -121,7 +121,7 @@ djangorestframework==3.13.1
# via -r requirements.in # via -r requirements.in
et-xmlfile==1.1.0 et-xmlfile==1.1.0
# via openpyxl # via openpyxl
fonttools[woff]==4.33.3 fonttools[woff]==4.34.4
# via weasyprint # via weasyprint
gunicorn==20.1.0 gunicorn==20.1.0
# via -r requirements.in # via -r requirements.in
@@ -135,7 +135,7 @@ itypes==1.2.0
# via coreapi # via coreapi
jinja2==3.1.2 jinja2==3.1.2
# via coreschema # via coreschema
markdown==3.3.7 markdown==3.4.1
# via django-markdownify # via django-markdownify
markuppy==1.14 markuppy==1.14
# via tablib # via tablib
@@ -149,7 +149,7 @@ openpyxl==3.0.10
# via tablib # via tablib
pdf2image==1.16.0 pdf2image==1.16.0
# via -r requirements.in # via -r requirements.in
pillow==9.1.1 pillow==9.2.0
# via # via
# -r requirements.in # -r requirements.in
# django-stdimage # django-stdimage
@@ -194,14 +194,14 @@ redis==3.5.3
# via # via
# django-q # django-q
# django-redis # django-redis
requests==2.28.0 requests==2.28.1
# via # via
# coreapi # coreapi
# django-allauth # django-allauth
# requests-oauthlib # requests-oauthlib
requests-oauthlib==1.3.1 requests-oauthlib==1.3.1
# via django-allauth # via django-allauth
sentry-sdk==1.6.0 sentry-sdk==1.9.0
# via -r requirements.in # via -r requirements.in
six==1.16.0 six==1.16.0
# via # via
@@ -224,13 +224,13 @@ tinycss2==1.1.1
# weasyprint # weasyprint
uritemplate==4.1.1 uritemplate==4.1.1
# via coreapi # via coreapi
urllib3==1.26.9 urllib3==1.26.11
# via # via
# requests # requests
# sentry-sdk # sentry-sdk
wcwidth==0.2.5 wcwidth==0.2.5
# via blessed # via blessed
weasyprint==55.0 weasyprint==56.1
# via django-weasyprint # via django-weasyprint
webencodings==0.5.1 webencodings==0.5.1
# via # via
@@ -242,7 +242,7 @@ xlrd==2.0.1
# via tablib # via tablib
xlwt==1.3.0 xlwt==1.3.0
# via tablib # via tablib
zipp==3.8.0 zipp==3.8.1
# via importlib-metadata # via importlib-metadata
zopfli==0.2.1 zopfli==0.2.1
# via fonttools # via fonttools