2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-18 04:55:44 +00:00

Merge branch 'master' of https://github.com/inventree/InvenTree into matmair/issue6281

This commit is contained in:
Matthias Mair
2024-04-08 18:41:57 +02:00
43 changed files with 809 additions and 374 deletions

27
src/backend/.eslintrc.yml Normal file
View File

@ -0,0 +1,27 @@
env:
commonjs: false
browser: true
es2021: true
jquery: true
extends:
- eslint:recommended
parserOptions:
ecmaVersion: 12
rules:
no-var: off
guard-for-in: off
no-trailing-spaces: off
camelcase: off
padded-blocks: off
prefer-const: off
max-len: off
require-jsdoc: off
valid-jsdoc: off
no-multiple-empty-lines: off
comma-dangle: off
no-unused-vars: off
no-useless-escape: off
prefer-spread: off
indent:
- error
- 4

View File

@ -84,25 +84,48 @@ class InvenTreeResource(ModelResource):
return [f for f in fields if f.column_name not in fields_to_exclude]
def before_import(self, dataset, using_transactions, dry_run, **kwargs):
"""Run custom code before importing data.
- Determine the list of fields which need to be converted to empty strings
"""
# Construct a map of field names
db_fields = {field.name: field for field in self.Meta.model._meta.fields}
for field_name, field in self.fields.items():
# Skip read-only fields (they cannot be imported)
if field.readonly:
continue
# Determine the name of the associated column in the dataset
column = getattr(field, 'column_name', field_name)
# Determine the attribute name of the associated database field
attribute = getattr(field, 'attribute', field_name)
# Check if the associated database field is a non-nullable string
if db_field := db_fields.get(attribute):
if (
isinstance(db_field, CharField)
and db_field.blank
and not db_field.null
):
if column not in self.CONVERT_NULL_FIELDS:
self.CONVERT_NULL_FIELDS.append(column)
return super().before_import(dataset, using_transactions, dry_run, **kwargs)
def before_import_row(self, row, row_number=None, **kwargs):
"""Run custom code before importing each row.
- Convert any null fields to empty strings, for fields which do not support null values
"""
# We can automatically determine which fields might need such a conversion
for field in self.Meta.model._meta.fields:
if (
isinstance(field, CharField)
and field.blank
and not field.null
and field.name not in self.CONVERT_NULL_FIELDS
):
self.CONVERT_NULL_FIELDS.append(field.name)
for field in self.CONVERT_NULL_FIELDS:
if field in row and row[field] is None:
row[field] = ''
return super().before_import_row(row, row_number, **kwargs)
class CustomRateAdmin(RateAdmin):
"""Admin interface for the Rate class."""

View File

@ -1,6 +1,9 @@
"""Main JSON interface views."""
import json
import logging
import sys
from pathlib import Path
from django.conf import settings
from django.db import transaction
@ -31,6 +34,60 @@ from .status import check_system_health, is_worker_running
from .version import inventreeApiText
from .views import AjaxView
logger = logging.getLogger('inventree')
class LicenseViewSerializer(serializers.Serializer):
"""Serializer for license information."""
backend = serializers.CharField(help_text='Backend licenses texts', read_only=True)
frontend = serializers.CharField(
help_text='Frontend licenses texts', read_only=True
)
class LicenseView(APIView):
"""Simple JSON endpoint for InvenTree license information."""
permission_classes = [permissions.IsAuthenticated]
def read_license_file(self, path: Path) -> list:
"""Extract license information from the provided file.
Arguments:
path: Path to the license file
Returns: A list of items containing the license information
"""
# Check if the file exists
if not path.exists():
logger.error("License file not found at '%s'", path)
return []
try:
data = json.loads(path.read_text())
except json.JSONDecodeError as e:
logger.exception("Failed to parse license file '%s': %s", path, e)
return []
except Exception as e:
logger.exception("Exception while reading license file '%s': %s", path, e)
return []
# Ensure consistent string between backend and frontend licenses
return [{key.lower(): value for key, value in entry.items()} for entry in data]
@extend_schema(responses={200: OpenApiResponse(response=LicenseViewSerializer)})
def get(self, request, *args, **kwargs):
"""Return information about the InvenTree server."""
backend = Path(__file__).parent.joinpath('licenses.txt')
frontend = Path(__file__).parent.parent.joinpath(
'web/static/web/.vite/dependencies.json'
)
return JsonResponse({
'backend': self.read_license_file(backend),
'frontend': self.read_license_file(frontend),
})
class VersionViewSerializer(serializers.Serializer):
"""Serializer for a single version."""

View File

@ -1,11 +1,14 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 185
INVENTREE_API_VERSION = 186
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v186 - 2024-03-26 : https://github.com/inventree/InvenTree/pull/6855
- Adds license information to the API
v185 - 2024-03-24 : https://github.com/inventree/InvenTree/pull/6836
- Remove /plugin/activate endpoint
- Update docstrings and typing for various API endpoints (no functional changes)

View File

@ -265,6 +265,26 @@ MIDDLEWARE = CONFIG.get(
],
)
# In DEBUG mode, add support for django-querycount
# Ref: https://github.com/bradmontgomery/django-querycount
if DEBUG and get_boolean_setting(
'INVENTREE_DEBUG_QUERYCOUNT', 'debug_querycount', False
):
MIDDLEWARE.append('querycount.middleware.QueryCountMiddleware')
QUERYCOUNT = {
'THRESHOLDS': {
'MEDIUM': 50,
'HIGH': 200,
'MIN_TIME_TO_LOG': 0,
'MIN_QUERY_COUNT_TO_LOG': 0,
},
'IGNORE_REQUEST_PATTERNS': ['^(?!\/(api)?(plugin)?\/).*'],
'IGNORE_SQL_PATTERNS': [],
'DISPLAY_DUPLICATES': 3,
'RESPONSE_HEADER': 'X-Django-Query-Count',
}
AUTHENTICATION_BACKENDS = CONFIG.get(
'authentication_backends',
[

View File

@ -1148,12 +1148,8 @@ class TestSettings(InvenTreeTestCase):
superuser = True
def in_env_context(self, envs=None):
def in_env_context(self, envs):
"""Patch the env to include the given dict."""
# Set default - see B006
if envs is None:
envs = {}
return mock.patch.dict(os.environ, envs)
def run_reload(self, envs=None):
@ -1588,15 +1584,15 @@ class ClassValidationMixinTest(TestCase):
def test(self):
"""Test function."""
pass
...
def test1(self):
"""Test function."""
pass
...
def test2(self):
"""Test function."""
pass
...
required_attributes = ['NAME']
required_overrides = [test, [test1, test2]]
@ -1616,11 +1612,11 @@ class ClassValidationMixinTest(TestCase):
def test(self):
"""Test function."""
pass
...
def test2(self):
"""Test function."""
pass
...
TestClass.validate()
@ -1643,7 +1639,7 @@ class ClassValidationMixinTest(TestCase):
def test2(self):
"""Test function."""
pass
...
with self.assertRaisesRegex(
NotImplementedError,

View File

@ -157,12 +157,14 @@ class UserMixin:
if type(assign_all) is not bool:
# Raise exception if common mistake is made!
raise TypeError('assignRole: assign_all must be a boolean value')
raise TypeError(
'assignRole: assign_all must be a boolean value'
) # pragma: no cover
if not role and not assign_all:
raise ValueError(
'assignRole: either role must be provided, or assign_all must be set'
)
) # pragma: no cover
if not assign_all and role:
rule, perm = role.split('.')
@ -241,14 +243,18 @@ class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase):
yield # your test will be run here
if verbose:
msg = '\r\n%s' % json.dumps(context.captured_queries, indent=4)
msg = '\r\n%s' % json.dumps(
context.captured_queries, indent=4
) # pragma: no cover
else:
msg = None
n = len(context.captured_queries)
if debug:
print(f'Expected less than {value} queries, got {n} queries')
print(
f'Expected less than {value} queries, got {n} queries'
) # pragma: no cover
self.assertLess(n, value, msg=msg)
@ -258,7 +264,7 @@ class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase):
if expected_code is None:
return
if expected_code != response.status_code:
if expected_code != response.status_code: # pragma: no cover
print(
f"Unexpected {method} response at '{url}': status_code = {response.status_code}"
)
@ -280,11 +286,7 @@ class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase):
response = self.client.options(url)
self.assertEqual(response.status_code, 200)
actions = response.data.get('actions', None)
if not actions:
actions = {}
actions = response.data.get('actions', {})
return actions
def get(self, url, data=None, expected_code=200, format='json', **kwargs):

View File

@ -39,7 +39,14 @@ from stock.urls import stock_urls
from web.urls import api_urls as web_api_urls
from web.urls import urlpatterns as platform_urls
from .api import APISearchView, InfoView, NotFoundView, VersionTextView, VersionView
from .api import (
APISearchView,
InfoView,
LicenseView,
NotFoundView,
VersionTextView,
VersionView,
)
from .magic_login import GetSimpleLoginView
from .social_auth_urls import (
EmailListView,
@ -99,6 +106,7 @@ apipatterns = [
name='schema',
),
# InvenTree information endpoints
path('license/', LicenseView.as_view(), name='api-license'), # license info
path(
'version-text', VersionTextView.as_view(), name='api-version-text'
), # version text
@ -377,6 +385,7 @@ if settings.ENABLE_CLASSIC_FRONTEND:
classic_frontendpatterns = [
# Apps
#
path('build/', include(build_urls)),
path('common/', include(common_urls)),
path('company/', include(company_urls)),

View File

@ -46,7 +46,7 @@ class NewsFeedTests(TestCase):
"""Tests that news feed is updated when accessing a valid URL."""
try:
common_tasks.update_news_feed()
except Exception as ex:
except Exception as ex: # pragma: no cover
self.fail(f'News feed raised exceptions: {ex}')
self.assertNotEqual(NewsFeedEntry.objects.all().count(), 0)

View File

@ -1048,18 +1048,18 @@ class ColorThemeTest(TestCase):
"""Test that default choices are returned."""
result = ColorTheme.get_color_themes_choices()
# skip
# skip due to directories not being set up
if not result:
return
return # pragma: no cover
self.assertIn(('default', 'Default'), result)
def test_valid_choice(self):
"""Check that is_valid_choice works correctly."""
result = ColorTheme.get_color_themes_choices()
# skip
# skip due to directories not being set up
if not result:
return
return # pragma: no cover
# check wrong reference
self.assertFalse(ColorTheme.is_valid_choice('abcdd'))
@ -1099,10 +1099,12 @@ class CurrencyAPITests(InvenTreeAPITestCase):
# Exit early
return
# Delay and try again
time.sleep(10)
# Delay and try again - might have problems with exchange rate endpoint
time.sleep(10) # pragma: no cover
raise TimeoutError('Could not refresh currency exchange data after 5 attempts')
raise TimeoutError(
'Could not refresh currency exchange data after 5 attempts'
) # pragma: no cover
class NotesImageTest(InvenTreeAPITestCase):

View File

@ -5,9 +5,6 @@ from InvenTree.unit_test import InvenTreeTestCase
from .transition import StateTransitionMixin, TransitionMethod, storage
# Global variables to determine which transition classes raises an exception
global raise_storage
global raise_function
raise_storage = False
raise_function = False
@ -90,7 +87,7 @@ class TransitionTests(InvenTreeTestCase):
if raise_function:
return 1234
else:
return False
return False # pragma: no cover # Return false to keep other transitions working
storage.collect()
self.assertIn(ValidImplementationNoEffect, storage.list)

View File

@ -23,7 +23,7 @@ class GeneralStatus(StatusCode):
def GHI(self): # This should be ignored
"""A invalid function."""
pass
...
class GeneralStateTest(InvenTreeTestCase):

View File

@ -64,11 +64,6 @@ class LabelTest(InvenTreeAPITestCase):
response = self.get(url, {'enabled': False})
self.assertEqual(len(response.data), 0)
# Disable each report
for label in labels:
label.enabled = False
label.save()
# Filter by "enabled" status
response = self.get(url, {'enabled': True})
self.assertEqual(len(response.data), 0)

View File

@ -143,7 +143,7 @@ class MachineAPITest(TestMachineRegistryMixin, InvenTreeAPITestCase):
for error in errors_msgs:
if re.match(pattern, error):
break
else:
else: # pragma: no cover
errors_str = '\n'.join([f'- {e}' for e in errors_msgs])
self.fail(
f"""Error message matching pattern '{pattern}' not found in machine registry errors:\n{errors_str}"""

View File

@ -272,15 +272,21 @@ class TestLabelPrinterMachineType(TestMachineRegistryMixin, InvenTreeAPITestCase
self.print_labels.assert_called_once()
self.assertEqual(self.print_labels.call_args.args[0], self.machine.machine)
self.assertEqual(self.print_labels.call_args.args[1], label)
self.assertQuerySetEqual(
self.print_labels.call_args.args[2], parts, transform=lambda x: x
)
# TODO re-activate test
# self.assertQuerySetEqual(
# self.print_labels.call_args.args[2], parts, transform=lambda x: x
# )
self.assertIn('printing_options', self.print_labels.call_args.kwargs)
self.assertEqual(
self.print_labels.call_args.kwargs['printing_options'],
{'copies': 1, 'test_option': 2},
)
return
# TODO re-activate test
# test the single print label method calls
self.assertEqual(self.print_label.call_count, 2)
self.assertEqual(self.print_label.call_args.args[0], self.machine.machine)

View File

@ -62,16 +62,19 @@ class SettingsMixin:
"""Does this plugin use custom global settings."""
return bool(self.settings)
def get_setting(self, key, cache=False):
def get_setting(self, key, cache=False, backup_value=None):
"""Return the 'value' of the setting associated with this plugin.
Arguments:
key: The 'name' of the setting value to be retrieved
cache: Whether to use RAM cached value (default = False)
backup_value: A backup value to return if the setting is not found
"""
from plugin.models import PluginSetting
return PluginSetting.get_setting(key, plugin=self.plugin_config(), cache=cache)
return PluginSetting.get_setting(
key, plugin=self.plugin_config(), cache=cache, backup_value=backup_value
)
def set_setting(self, key, value, user=None):
"""Set plugin setting value by key."""

View File

@ -11,6 +11,15 @@ from stock.models import StockItem, StockLocation
class LocatePluginTests(InvenTreeAPITestCase):
"""Tests for LocateMixin."""
def setUp(self):
"""Set up the test case."""
super().setUp()
# Activate plugin
config = registry.get_plugin('samplelocate').plugin_config()
config.active = True
config.save()
fixtures = ['category', 'part', 'location', 'stock']
def test_installed(self):

View File

@ -554,9 +554,6 @@ class StockItemListTest(StockAPITestCase):
)
self.assertTrue(len(response.data) < StockItem.objects.count())
for result in response.data:
self.assertIsNone(result['location'])
# Filter with "cascade=True"
response = self.get(
self.list_url, {'location': 'null', 'cascade': True}, expected_code=200

View File

@ -26,6 +26,11 @@ class TemplateTagTest(InvenTreeTestCase):
def test_spa_bundle(self):
"""Test the 'spa_bundle' template tag."""
resp = spa_helper.spa_bundle()
if not resp:
# No Vite, no test
# TODO: Add a test for the non-Vite case (docker)
return # pragma: no cover
shipped_js = resp.split('<script type="module" src="')[1:]
self.assertTrue(len(shipped_js) > 0)
self.assertTrue(len(shipped_js) == 3)

View File

@ -1,5 +1,5 @@
{
"name": "InvenTree",
"name": "backend",
"lockfileVersion": 3,
"requires": true,
"packages": {
@ -95,9 +95,9 @@
}
},
"node_modules/@humanwhocodes/object-schema": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz",
"integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw=="
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
"integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
@ -472,9 +472,9 @@
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="
},
"node_modules/fastq": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
"integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
"integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
"dependencies": {
"reusify": "^1.0.4"
}
@ -506,11 +506,12 @@
}
},
"node_modules/flat-cache": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
"integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
"integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
"dependencies": {
"flatted": "^3.1.0",
"flatted": "^3.2.9",
"keyv": "^4.5.3",
"rimraf": "^3.0.2"
},
"engines": {
@ -518,9 +519,9 @@
}
},
"node_modules/flatted": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ=="
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw=="
},
"node_modules/fs.realpath": {
"version": "1.0.0",
@ -672,6 +673,11 @@
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@ -682,6 +688,14 @@
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
"dependencies": {
"json-buffer": "3.0.1"
}
},
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",

View File

@ -2,6 +2,7 @@
-c requirements.txt
coverage[toml] # Unit test coverage
coveralls==2.1.2 # Coveralls linking (for tracking coverage) # PINNED 2022-06-28 - Old version needed for correct upload
django-querycount # Display number of URL queries for requests
django-slowtests # Show which unit tests are running slowly
django-test-migrations # Unit testing for database migrations
isort # python import sorting

View File

@ -1,8 +1,8 @@
# This file was autogenerated by uv via the following command:
# uv pip compile src/backend/requirements-dev.in -o src/backend/requirements-dev.txt --python-version=3.9 --no-strip-extras
asgiref==3.8.0
asgiref==3.8.1
# via django
build==1.1.1
build==1.2.1
# via pip-tools
certifi==2024.2.2
# via requests
@ -25,17 +25,18 @@ distlib==0.3.8
# via virtualenv
django==4.2.11
# via django-slowtests
django-querycount==0.8.3
django-slowtests==1.1.1
django-test-migrations==1.3.0
docopt==0.6.2
# via coveralls
filelock==3.13.1
filelock==3.13.3
# via virtualenv
identify==2.5.35
# via pre-commit
idna==3.6
# via requests
importlib-metadata==6.11.0
importlib-metadata==7.0.0
# via build
isort==5.13.2
nodeenv==1.8.0
@ -49,7 +50,7 @@ pip-tools==7.4.1
platformdirs==4.2.0
# via virtualenv
pre-commit==3.7.0
pycparser==2.21
pycparser==2.22
# via cffi
pyproject-hooks==1.0.0
# via
@ -72,7 +73,7 @@ tomli==2.0.1
# build
# pip-tools
# pyproject-hooks
typing-extensions==4.10.0
typing-extensions==4.11.0
# via
# asgiref
# django-test-migrations

View File

@ -17,7 +17,7 @@ django-maintenance-mode # Shut down application while reloading
django-markdownify # Markdown rendering
django-mptt # Modified Preorder Tree Traversal
django-markdownify # Markdown rendering
django-money>=3.0.0,<3.5.0 # Django app for currency management # FIXED 2023-10-31 3.3.0 breaks due to https://github.com/django-money/django-money/issues/731
django-money>=3.0.0,<3.3.0 # Django app for currency management # FIXED 2023-10-31 3.3.0 breaks due to https://github.com/django-money/django-money/issues/731
django-mptt # Modified Preorder Tree Traversal
django-redis>=5.0.0 # Redis integration
django-q2 # Background task scheduling
@ -41,6 +41,7 @@ gunicorn # Gunicorn web server
pdf2image # PDF to image conversion
pillow # Image manipulation
pint==0.21 # Unit conversion # FIXED 2023-05-30 breaks tests https://github.com/matmair/InvenTree/actions/runs/5095665936/jobs/9160852560
pip-licenses # License information for installed packages
python-barcode[images] # Barcode generator
python-dotenv # Environment variable management
pyyaml>=6.0.1 # YAML parsing

View File

@ -1,6 +1,6 @@
# This file was autogenerated by uv via the following command:
# uv pip compile src/backend/requirements.in -o src/backend/requirements.txt --python-version=3.9 --no-strip-extras
asgiref==3.8.0
asgiref==3.8.1
# via
# django
# django-cors-headers
@ -84,7 +84,7 @@ django-cors-headers==4.3.1
django-crispy-forms==1.14.0
django-dbbackup==4.1.0
django-error-report-2==0.4.2
django-filter==24.1
django-filter==24.2
django-flags==5.0.13
django-formtools==2.5.1
django-ical==1.9.2
@ -96,7 +96,7 @@ django-markdownify==0.9.3
django-money==3.2.0
django-mptt==0.16.0
django-otp==1.3.0
django-picklefield==3.1
django-picklefield==3.2
# via django-q2
django-q-sentry==0.1.6
django-q2==1.6.2
@ -117,12 +117,12 @@ djangorestframework==3.14.0
# djangorestframework-simplejwt
# drf-spectacular
djangorestframework-simplejwt[crypto]==5.3.1
drf-spectacular==0.27.1
drf-spectacular==0.27.2
dulwich==0.21.7
et-xmlfile==1.1.0
# via openpyxl
feedparser==6.0.11
fonttools[woff]==4.50.0
fonttools[woff]==4.51.0
# via weasyprint
googleapis-common-protos==1.63.0
# via
@ -137,7 +137,7 @@ icalendar==5.0.12
# via django-ical
idna==3.6
# via requests
importlib-metadata==6.11.0
importlib-metadata==7.0.0
# via
# django-q2
# markdown
@ -164,7 +164,7 @@ odfpy==1.4.1
# via tablib
openpyxl==3.1.2
# via tablib
opentelemetry-api==1.23.0
opentelemetry-api==1.24.0
# via
# opentelemetry-exporter-otlp-proto-grpc
# opentelemetry-exporter-otlp-proto-http
@ -174,43 +174,43 @@ opentelemetry-api==1.23.0
# opentelemetry-instrumentation-requests
# opentelemetry-instrumentation-wsgi
# opentelemetry-sdk
opentelemetry-exporter-otlp==1.23.0
opentelemetry-exporter-otlp-proto-common==1.23.0
opentelemetry-exporter-otlp==1.24.0
opentelemetry-exporter-otlp-proto-common==1.24.0
# via
# opentelemetry-exporter-otlp-proto-grpc
# opentelemetry-exporter-otlp-proto-http
opentelemetry-exporter-otlp-proto-grpc==1.23.0
opentelemetry-exporter-otlp-proto-grpc==1.24.0
# via opentelemetry-exporter-otlp
opentelemetry-exporter-otlp-proto-http==1.23.0
opentelemetry-exporter-otlp-proto-http==1.24.0
# via opentelemetry-exporter-otlp
opentelemetry-instrumentation==0.44b0
opentelemetry-instrumentation==0.45b0
# via
# opentelemetry-instrumentation-django
# opentelemetry-instrumentation-redis
# opentelemetry-instrumentation-requests
# opentelemetry-instrumentation-wsgi
opentelemetry-instrumentation-django==0.44b0
opentelemetry-instrumentation-redis==0.44b0
opentelemetry-instrumentation-requests==0.44b0
opentelemetry-instrumentation-wsgi==0.44b0
opentelemetry-instrumentation-django==0.45b0
opentelemetry-instrumentation-redis==0.45b0
opentelemetry-instrumentation-requests==0.45b0
opentelemetry-instrumentation-wsgi==0.45b0
# via opentelemetry-instrumentation-django
opentelemetry-proto==1.23.0
opentelemetry-proto==1.24.0
# via
# opentelemetry-exporter-otlp-proto-common
# opentelemetry-exporter-otlp-proto-grpc
# opentelemetry-exporter-otlp-proto-http
opentelemetry-sdk==1.23.0
opentelemetry-sdk==1.24.0
# via
# opentelemetry-exporter-otlp-proto-grpc
# opentelemetry-exporter-otlp-proto-http
opentelemetry-semantic-conventions==0.44b0
opentelemetry-semantic-conventions==0.45b0
# via
# opentelemetry-instrumentation-django
# opentelemetry-instrumentation-redis
# opentelemetry-instrumentation-requests
# opentelemetry-instrumentation-wsgi
# opentelemetry-sdk
opentelemetry-util-http==0.44b0
opentelemetry-util-http==0.45b0
# via
# opentelemetry-instrumentation-django
# opentelemetry-instrumentation-requests
@ -218,7 +218,7 @@ opentelemetry-util-http==0.44b0
packaging==24.0
# via gunicorn
pdf2image==1.17.0
pillow==10.2.0
pillow==10.3.0
# via
# django-stdimage
# pdf2image
@ -226,13 +226,16 @@ pillow==10.2.0
# qrcode
# weasyprint
pint==0.21
pip-licenses==4.4.0
prettytable==3.10.0
# via pip-licenses
protobuf==4.25.3
# via
# googleapis-common-protos
# opentelemetry-proto
py-moneyed==3.0
# via django-money
pycparser==2.21
pycparser==2.22
# via cffi
pydyf==0.9.0
# via weasyprint
@ -279,13 +282,13 @@ requests==2.31.0
# django-allauth
# opentelemetry-exporter-otlp-proto-http
# requests-oauthlib
requests-oauthlib==1.4.0
requests-oauthlib==2.0.0
# via django-allauth
rpds-py==0.18.0
# via
# jsonschema
# referencing
sentry-sdk==1.43.0
sentry-sdk==1.44.1
# via django-q-sentry
setuptools==69.2.0
# via
@ -309,7 +312,7 @@ tinycss2==1.2.1
# bleach
# cssselect2
# weasyprint
typing-extensions==4.10.0
typing-extensions==4.11.0
# via
# asgiref
# drf-spectacular
@ -325,6 +328,8 @@ urllib3==2.2.1
# dulwich
# requests
# sentry-sdk
wcwidth==0.2.13
# via prettytable
weasyprint==61.2
# via django-weasyprint
webencodings==0.5.1