2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-05-23 01:25:45 +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 PIP_USER='no'
sudo apt install gettext
sudo apt install -y gettext
python3 -m venv venv
source venv/bin/activate
pip install invoke
pip install invoke pyyaml
mkdir dev
inv setup-test
invoke update
invoke setup-test --ignore-update --path inventree-data
gp sync-done start_server
- name: Start server
+5 -1
View File
@@ -2,11 +2,15 @@
# 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
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
- Updates the PartCategory list API:
- 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.core import validators
from django.db import models as models
from django.forms.fields import URLField as FormURLField
from django.utils.translation import gettext_lazy as _
from djmoney.forms.fields import MoneyField
@@ -23,26 +22,28 @@ class InvenTreeRestURLField(RestURLField):
"""Custom field for DRF with custom scheme vaildators."""
def __init__(self, **kwargs):
"""Update schemes."""
# Enforce 'max length' parameter in form validation
if 'max_length' not in kwargs:
kwargs['max_length'] = 200
super().__init__(**kwargs)
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):
"""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):
"""Return a Field instance for this field."""
return super().formfield(**{
'form_class': InvenTreeURLFormField
})
def __init__(self, **kwargs):
"""Initialization method for InvenTreeURLField"""
# Max length for InvenTreeURLField defaults to 200
if 'max_length' not in kwargs:
kwargs['max_length'] = 200
super().__init__(**kwargs)
def money_kwargs():
+36 -9
View File
@@ -12,6 +12,7 @@ from wsgiref.util import FileWrapper
from django.conf import settings
from django.contrib.auth.models import Permission
from django.contrib.staticfiles.storage import StaticFilesStorage
from django.core.exceptions import FieldError, ValidationError
from django.core.files.storage import default_storage
from django.core.validators import URLValidator
@@ -241,17 +242,27 @@ def getLogoImage(as_file=False, custom=True):
"""Return the path to the logo-file."""
if custom and settings.CUSTOM_LOGO:
if as_file:
return f"file://{default_storage.path(settings.CUSTOM_LOGO)}"
else:
return default_storage.url(settings.CUSTOM_LOGO)
static_storage = StaticFilesStorage()
else:
if as_file:
path = settings.STATIC_ROOT.joinpath('img/inventree.png')
return f"file://{path}"
if static_storage.exists(settings.CUSTOM_LOGO):
storage = static_storage
elif default_storage.exists(settings.CUSTOM_LOGO):
storage = default_storage
else:
return getStaticUrl('img/inventree.png')
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:
path = settings.STATIC_ROOT.joinpath('img/inventree.png')
return f"file://{path}"
else:
return getStaticUrl('img/inventree.png')
def TestIfImageURL(url):
@@ -283,6 +294,22 @@ def str2bool(text, test=True):
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):
"""Determine if a string value 'looks' like a boolean."""
if str2bool(text, True):
+10 -6
View File
@@ -116,6 +116,12 @@ class InvenTreeMetadata(SimpleMetadata):
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:
model_class = serializer.Meta.model
@@ -148,10 +154,7 @@ class InvenTreeMetadata(SimpleMetadata):
elif name in model_default_values:
serializer_info[name]['default'] = model_default_values[name]
# Attributes to copy from the model to the field (if they don't exist)
attributes = ['help_text']
for attr in attributes:
for attr in extra_attributes:
if attr not in serializer_info[name]:
if hasattr(field, attr):
@@ -172,8 +175,9 @@ class InvenTreeMetadata(SimpleMetadata):
# This is used to automatically filter AJAX requests
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'):
serializer_info[name]['help_text'] = relation.model_field.help_text
for attr in extra_attributes:
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:
serializer_info[name]['default'] = model_default_values[name]
+20 -6
View File
@@ -16,6 +16,7 @@ import sys
from pathlib import Path
import django.conf.locale
from django.contrib.staticfiles.storage import StaticFilesStorage
from django.core.files.storage import default_storage
from django.http import Http404
from django.utils.translation import gettext_lazy as _
@@ -127,7 +128,7 @@ STATFILES_I18_PROCESSORS = [
]
# 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
MEDIA_URL = '/media/'
@@ -135,6 +136,8 @@ MEDIA_URL = '/media/'
# Application definition
INSTALLED_APPS = [
# Admin site integration
'django.contrib.admin',
# InvenTree apps
'build.apps.BuildConfig',
@@ -150,7 +153,6 @@ INSTALLED_APPS = [
'InvenTree.apps.InvenTreeConfig', # InvenTree app runs last
# Core django modules
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'user_sessions', # db user sessions
@@ -820,10 +822,22 @@ CUSTOMIZE = get_setting('INVENTREE_CUSTOMIZE', 'customize', {})
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
logger.warning(f"The custom logo file '{CUSTOM_LOGO}' could not be found in the default media storage")
CUSTOM_LOGO = False
"""
Check for the existence of a 'custom logo' file:
- 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
if DEBUG:
logger.info("InvenTree running with DEBUG enabled")
+13
View File
@@ -119,3 +119,16 @@ class ViewTests(InvenTreeTestCase):
for panel in staff_panels + plugin_panels:
self.assertNotIn(f"select-{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 .views import (AboutView, AppearanceSelectView, CurrencyRefreshView,
CustomConnectionsView, CustomEmailView,
CustomConnectionsView, CustomEmailView, CustomLoginView,
CustomPasswordResetFromKeyView,
CustomSessionDeleteOtherView, CustomSessionDeleteView,
CustomTwoFactorRemove, DatabaseStatsView, DynamicJsView,
@@ -168,6 +168,9 @@ frontendpatterns = [
# 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'),
# 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.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.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.views import ConnectionsView
from allauth_2fa.views import TwoFactorRemove
@@ -700,6 +701,23 @@ class CustomSessionDeleteOtherView(UserSessionOverride, SessionDeleteOtherView):
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):
"""POST endpoint to refresh / update exchange rates."""
+14 -4
View File
@@ -10,7 +10,6 @@ import hmac
import json
import logging
import math
import os
import uuid
from datetime import datetime, timedelta
from enum import Enum
@@ -877,6 +876,16 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'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': {
'name': _('Barcode Support'),
'description': _('Enable barcode scanner support'),
@@ -1775,14 +1784,15 @@ class ColorTheme(models.Model):
@classmethod
def get_color_themes_choices(cls):
"""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')
return []
# Get files list from css/color-themes/ folder
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)
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?
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:
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,
ListCreateDestroyAPIView)
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,
RetrieveUpdateAPI, RetrieveUpdateDestroyAPI,
UpdateAPI)
@@ -85,6 +86,8 @@ class CategoryList(ListCreateAPI):
cascade = str2bool(params.get('cascade', False))
depth = str2int(params.get('depth', None))
# Do not filter by category
if cat_id is None:
pass
@@ -94,12 +97,18 @@ class CategoryList(ListCreateAPI):
if not cascade:
queryset = queryset.filter(parent=None)
if cascade and depth is not None:
queryset = queryset.filter(level__lte=depth)
else:
try:
category = PartCategory.objects.get(pk=cat_id)
if cascade:
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]
queryset = queryset.filter(parent__in=parent_ids)
+30 -26
View File
@@ -49,33 +49,36 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
"""Test the PartCategoryList API endpoint"""
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
response = self.get(
url,
{
'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)
for params, res_len, description in test_cases:
response = self.get(url, params, expected_code=200)
self.assertEqual(len(response.data), res_len, description)
# Check that the required fields are present
fields = [
@@ -90,9 +93,10 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
'url'
]
response = self.get(url, expected_code=200)
for result in response.data:
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):
"""Test that the 'part_count' field is annotated correctly"""
+9 -1
View File
@@ -26,7 +26,7 @@ from InvenTree.api import (APIDownloadMixin, AttachmentMixin,
ListCreateDestroyAPIView)
from InvenTree.filters import InvenTreeOrderingFilter
from InvenTree.helpers import (DownloadFile, extract_serial_numbers, isNull,
str2bool)
str2bool, str2int)
from InvenTree.mixins import (CreateAPI, ListAPI, ListCreateAPI, RetrieveAPI,
RetrieveUpdateAPI, RetrieveUpdateDestroyAPI)
from order.models import PurchaseOrder, SalesOrder, SalesOrderAllocation
@@ -241,6 +241,8 @@ class StockLocationList(ListCreateAPI):
cascade = str2bool(params.get('cascade', False))
depth = str2int(params.get('depth', None))
# Do not filter by location
if loc_id is None:
pass
@@ -251,6 +253,9 @@ class StockLocationList(ListCreateAPI):
if not cascade:
queryset = queryset.filter(parent=None)
if cascade and depth is not None:
queryset = queryset.filter(level__lte=depth)
else:
try:
@@ -259,6 +264,9 @@ class StockLocationList(ListCreateAPI):
# All sub-locations to be returned too?
if cascade:
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]
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(
verbose_name=_('External Link'),
max_length=125, blank=True,
blank=True, max_length=200,
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')
def test_list(self):
"""Test StockLocation list."""
# Check that we can request the StockLocation list
response = self.client.get(self.list_url, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertGreaterEqual(len(response.data), 1)
"""Test the StockLocationList API endpoint"""
test_cases = [
({}, 8, 'no parameters'),
({'parent': 1, 'cascade': False}, 2, 'Filter by parent, no cascading'),
({'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):
"""Test adding StockLocation."""
+39
View File
@@ -44,6 +44,45 @@ class StockTest(InvenTreeTestCase):
Part.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):
"""Test expiry date functionality for StockItem model."""
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_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_TREE_DEPTH" icon="fa-sitemap" %}
</tbody>
</table>
+57 -1
View File
@@ -1750,6 +1750,7 @@ function loadPartCategoryTable(table, options) {
if (tree_view) {
params.cascade = true;
params.depth = global_settings.INVENTREE_TREE_DEPTH;
}
var original = {};
@@ -1761,6 +1762,35 @@ function loadPartCategoryTable(table, options) {
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({
treeEnable: tree_view,
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 {
$('#view-category-tree').removeClass('btn-secondary').addClass('btn-outline-secondary');
$('#view-category-list').removeClass('btn-outline-secondary').addClass('btn-secondary');
@@ -1859,8 +1903,20 @@ function loadPartCategoryTable(table, options) {
switchable: true,
sortable: true,
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,
`/part/category/${row.pk}/`
);
+60 -1
View File
@@ -2226,6 +2226,7 @@ function loadStockLocationTable(table, options) {
if (tree_view) {
params.cascade = true;
params.depth = global_settings.INVENTREE_TREE_DEPTH;
}
var filters = {};
@@ -2248,6 +2249,35 @@ function loadStockLocationTable(table, options) {
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({
treeEnable: tree_view,
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 {
$('#view-location-tree').removeClass('btn-secondary').addClass('btn-outline-secondary');
$('#view-location-list').removeClass('btn-outline-secondary').addClass('btn-secondary');
@@ -2345,10 +2389,25 @@ function loadStockLocationTable(table, options) {
switchable: true,
sortable: true,
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,
`/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 = os.environ['GITHUB_REF']
GITHUB_REF_NAME = os.environ['GITHUB_REF_NAME']
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 = None
@@ -109,8 +117,19 @@ if __name__ == '__main__':
print(f"InvenTree Version: '{version}'")
# 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
highest_release = check_version_number(version, allow_duplicate=GITHUB_REF_TYPE == 'tag')
# If a release is found which matches the current tag, throw an error
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
docker_tags = None
+64 -56
View File
@@ -10,6 +10,10 @@ asgiref==3.5.2 \
# via
# -c requirements.txt
# django
build==0.8.0 \
--hash=sha256:19b0ed489f92ace6947698c3ca8436cb0556a66e2aa2d34cd70e2a5d27cd0437 \
--hash=sha256:887a6d471c901b1a6e6574ebaeeebb45e5269a79d095fe9a8f88d6614ed2e5f0
# via pip-tools
certifi==2022.6.15 \
--hash=sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d \
--hash=sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412
@@ -20,9 +24,9 @@ cfgv==3.3.1 \
--hash=sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426 \
--hash=sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736
# via pre-commit
charset-normalizer==2.0.12 \
--hash=sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597 \
--hash=sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df
charset-normalizer==2.1.0 \
--hash=sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5 \
--hash=sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413
# via
# -c requirements.txt
# requests
@@ -90,13 +94,13 @@ coveralls==2.1.2 \
--hash=sha256:4430b862baabb3cf090d36d84d331966615e4288d8a8c5957e0fd456d0dd8bd6 \
--hash=sha256:b3b60c17b03a0dee61952a91aed6f131e0b2ac8bd5da909389c53137811409e1
# via -r requirements-dev.in
distlib==0.3.4 \
--hash=sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b \
--hash=sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579
distlib==0.3.5 \
--hash=sha256:a7f75737c70be3b25e2bee06288cec4e4c221de18455b2dd037fe2a795cab2fe \
--hash=sha256:b710088c59f06338ca514800ad795a132da19fda270e3ce4affc74abf955a26c
# via virtualenv
django==3.2.14 \
--hash=sha256:677182ba8b5b285a4e072f3ac17ceee6aff1b5ce77fd173cc5b6a2d3dc022fcf \
--hash=sha256:a8681e098fa60f7c33a4b628d6fcd3fe983a0939ff1301ecacac21d0b38bad56
django==3.2.15 \
--hash=sha256:115baf5049d5cf4163e43492cdc7139c306ed6d451e7d3571fe9612903903713 \
--hash=sha256:f71934b1a822f14a86c9ac9634053689279cd04ae69cb6ade4a59471b886582b
# via
# -c requirements.txt
# django-debug-toolbar
@@ -115,9 +119,9 @@ filelock==3.7.1 \
--hash=sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404 \
--hash=sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04
# via virtualenv
flake8==4.0.1 \
--hash=sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d \
--hash=sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d
flake8==5.0.4 \
--hash=sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db \
--hash=sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248
# via
# -r requirements-dev.in
# flake8-docstrings
@@ -126,9 +130,9 @@ flake8-docstrings==1.6.0 \
--hash=sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde \
--hash=sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b
# via -r requirements-dev.in
identify==2.5.1 \
--hash=sha256:0dca2ea3e4381c435ef9c33ba100a78a9b40c0bab11189c7cf121f75815efeaa \
--hash=sha256:3d11b16f3fe19f52039fb7e39c9c884b21cb1b586988114fbe42671f03de3e82
identify==2.5.3 \
--hash=sha256:25851c8c1370effb22aaa3c987b30449e9ff0cece408f810ae6ce408fdd20893 \
--hash=sha256:887e7b91a1be152b0d46bbf072130235a8117392b9f1828446079a816a05ef44
# via pre-commit
idna==3.3 \
--hash=sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff \
@@ -140,46 +144,54 @@ isort==5.10.1 \
--hash=sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7 \
--hash=sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951
# via -r requirements-dev.in
mccabe==0.6.1 \
--hash=sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42 \
--hash=sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f
mccabe==0.7.0 \
--hash=sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325 \
--hash=sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e
# via flake8
nodeenv==1.7.0 \
--hash=sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e \
--hash=sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b
# via pre-commit
pep517==0.12.0 \
--hash=sha256:931378d93d11b298cf511dd634cf5ea4cb249a28ef84160b3247ee9afb4e8ab0 \
--hash=sha256:dd884c326898e2c6e11f9e0b64940606a93eb10ea022a2e067959f3a110cf161
# via pip-tools
pep8-naming==0.13.0 \
--hash=sha256:069ea20e97f073b3e6d4f789af2a57816f281ca64b86210c7d471117a4b6bfd0 \
--hash=sha256:9f38e6dcf867a1fb7ad47f5ff72c0ddae544a6cf64eb9f7600b7b3c0bb5980b5
packaging==21.3 \
--hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb \
--hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522
# via build
pep517==0.13.0 \
--hash=sha256:4ba4446d80aed5b5eac6509ade100bff3e7943a8489de249654a5ae9b33ee35b \
--hash=sha256:ae69927c5c172be1add9203726d4b84cf3ebad1edcd5f71fcdc746e66e829f59
# via build
pep8-naming==0.13.1 \
--hash=sha256:3af77cdaa9c7965f7c85a56cd579354553c9bbd3fdf3078a776f12db54dd6944 \
--hash=sha256:f7867c1a464fe769be4f972ef7b79d6df1d9aff1b1f04ecf738d471963d3ab9c
# via -r requirements-dev.in
pip-tools==6.6.2 \
--hash=sha256:6b486548e5a139e30e4c4a225b3b7c2d46942a9f6d1a91143c21b1de4d02fd9b \
--hash=sha256:f638503a9f77d98d9a7d72584b1508d3f82ed019b8fab24f4e5ad078c1b8c95e
pip-tools==6.8.0 \
--hash=sha256:39e8aee465446e02278d80dbebd4325d1dd8633248f43213c73a25f58e7d8a55 \
--hash=sha256:3e5cd4acbf383d19bdfdeab04738b6313ebf4ad22ce49bf529c729061eabfab8
# via -r requirements-dev.in
platformdirs==2.5.2 \
--hash=sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788 \
--hash=sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19
# via virtualenv
pre-commit==2.19.0 \
--hash=sha256:10c62741aa5704faea2ad69cb550ca78082efe5697d6f04e5710c3c229afdd10 \
--hash=sha256:4233a1e38621c87d9dda9808c6606d7e7ba0e087cd56d3fe03202a01d2919615
pre-commit==2.20.0 \
--hash=sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7 \
--hash=sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959
# via -r requirements-dev.in
pycodestyle==2.8.0 \
--hash=sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20 \
--hash=sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f
pycodestyle==2.9.1 \
--hash=sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785 \
--hash=sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b
# via flake8
pydocstyle==6.1.1 \
--hash=sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc \
--hash=sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4
# via flake8-docstrings
pyflakes==2.4.0 \
--hash=sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c \
--hash=sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e
pyflakes==2.5.0 \
--hash=sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2 \
--hash=sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3
# via flake8
pyparsing==3.0.9 \
--hash=sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb \
--hash=sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc
# via packaging
pytz==2022.1 \
--hash=sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7 \
--hash=sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c
@@ -223,18 +235,12 @@ pyyaml==6.0 \
# via
# -c requirements.txt
# pre-commit
requests==2.28.0 \
--hash=sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f \
--hash=sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b
requests==2.28.1 \
--hash=sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983 \
--hash=sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349
# via
# -c requirements.txt
# coveralls
six==1.16.0 \
--hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
--hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
# via
# -c requirements.txt
# virtualenv
snowballstemmer==2.2.0 \
--hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \
--hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a
@@ -253,20 +259,22 @@ toml==0.10.2 \
tomli==2.0.1 \
--hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
--hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
# via pep517
typing-extensions==4.2.0 \
--hash=sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708 \
--hash=sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376
# via
# build
# pep517
typing-extensions==4.3.0 \
--hash=sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02 \
--hash=sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6
# via django-test-migrations
urllib3==1.26.9 \
--hash=sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14 \
--hash=sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e
urllib3==1.26.11 \
--hash=sha256:c33ccba33c819596124764c23a97d25f32b28433ba0dedeb77d873a38722c9bc \
--hash=sha256:ea6e8fb210b19d950fab93b60c9009226c63a28808bc8386e05301e25883ac0a
# via
# -c requirements.txt
# requests
virtualenv==20.15.0 \
--hash=sha256:4c44b1d77ca81f8368e2d7414f9b20c428ad16b343ac6d226206c5b84e2b4fcc \
--hash=sha256:804cce4de5b8a322f099897e308eecc8f6e2951f1a8e7e2b3598dff865f01336
virtualenv==20.16.3 \
--hash=sha256:4193b7bc8a6cd23e4eb251ac64f29b4398ab2c233531e66e40b19a6b7b0d30c1 \
--hash=sha256:d86ea0bb50e06252d79e6c241507cb904fcd66090c3271381372d6221a3970f9
# via pre-commit
wheel==0.37.1 \
--hash=sha256:4bdcd7d840138086126cd09254dc6195fb4fc6f01c050a1d7236f2630db1d22a \
+12 -12
View File
@@ -10,7 +10,7 @@ asgiref==3.5.2
# via django
babel==2.10.3
# via py-moneyed
bleach[css]==5.0.0
bleach[css]==5.0.1
# via django-markdownify
blessed==1.19.1
# via django-q
@@ -20,11 +20,11 @@ certifi==2022.6.15
# via
# requests
# sentry-sdk
cffi==1.15.0
cffi==1.15.1
# via
# cryptography
# weasyprint
charset-normalizer==2.0.12
charset-normalizer==2.1.0
# via requests
coreapi==2.3.3
# via -r requirements.in
@@ -42,7 +42,7 @@ defusedxml==0.7.1
# python3-openid
diff-match-patch==20200713
# via django-import-export
django==3.2.14
django==3.2.15
# via
# -r requirements.in
# django-allauth
@@ -121,7 +121,7 @@ djangorestframework==3.13.1
# via -r requirements.in
et-xmlfile==1.1.0
# via openpyxl
fonttools[woff]==4.33.3
fonttools[woff]==4.34.4
# via weasyprint
gunicorn==20.1.0
# via -r requirements.in
@@ -135,7 +135,7 @@ itypes==1.2.0
# via coreapi
jinja2==3.1.2
# via coreschema
markdown==3.3.7
markdown==3.4.1
# via django-markdownify
markuppy==1.14
# via tablib
@@ -149,7 +149,7 @@ openpyxl==3.0.10
# via tablib
pdf2image==1.16.0
# via -r requirements.in
pillow==9.1.1
pillow==9.2.0
# via
# -r requirements.in
# django-stdimage
@@ -194,14 +194,14 @@ redis==3.5.3
# via
# django-q
# django-redis
requests==2.28.0
requests==2.28.1
# via
# coreapi
# django-allauth
# requests-oauthlib
requests-oauthlib==1.3.1
# via django-allauth
sentry-sdk==1.6.0
sentry-sdk==1.9.0
# via -r requirements.in
six==1.16.0
# via
@@ -224,13 +224,13 @@ tinycss2==1.1.1
# weasyprint
uritemplate==4.1.1
# via coreapi
urllib3==1.26.9
urllib3==1.26.11
# via
# requests
# sentry-sdk
wcwidth==0.2.5
# via blessed
weasyprint==55.0
weasyprint==56.1
# via django-weasyprint
webencodings==0.5.1
# via
@@ -242,7 +242,7 @@ xlrd==2.0.1
# via tablib
xlwt==1.3.0
# via tablib
zipp==3.8.0
zipp==3.8.1
# via importlib-metadata
zopfli==0.2.1
# via fonttools