2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-02 11:40:58 +00:00

[CI] Enable python autoformat (#6169)

* Squashed commit of the following:

commit f5cf7b2e78
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 20:36:57 2024 +0100

    fixed reqs

commit 9d845bee98
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 20:32:35 2024 +0100

    disable autofix/format

commit aff5f27148
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 20:28:50 2024 +0100

    adjust checks

commit 47271cf1ef
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 20:28:22 2024 +0100

    reorder order of operations

commit e1bf178b40
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 20:01:09 2024 +0100

    adapted ruff settings to better fit code base

commit ad7d88a6f4
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 19:59:45 2024 +0100

    auto fixed docstring

commit a2e54a760e
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 19:46:35 2024 +0100

    fix getattr useage

commit cb80c73bc6
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 19:25:09 2024 +0100

    fix requirements file

commit b7780bbd21
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:42:28 2024 +0100

    fix removed sections

commit 71f1681f55
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:41:21 2024 +0100

    fix djlint syntax

commit a0bcf1bcce
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:35:28 2024 +0100

    remove flake8 from code base

commit 22475b31cc
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:34:56 2024 +0100

    remove flake8 from code base

commit 0413350f14
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:24:39 2024 +0100

    moved ruff section

commit d90c48a0bf
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:24:24 2024 +0100

    move djlint config to pyproject

commit c5ce55d511
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:20:39 2024 +0100

    added isort again

commit 42a41d23af
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:19:02 2024 +0100

    move config section

commit 8569233181
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:17:52 2024 +0100

    fix codespell error

commit 2897c6704d
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 17:29:21 2024 +0100

    replaced flake8 with ruff
    mostly for speed improvements

* enable autoformat

* added autofixes

* switched to single quotes everywhere

* switched to ruff for import sorting

* fix wrong url response

* switched to pathlib for lookup

* fixed lookup

* Squashed commit of the following:

commit d3b795824b
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 22:56:17 2024 +0100

    fixed source path

commit 0bac0c19b8
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 22:47:53 2024 +0100

    fixed req

commit 9f61f01d9c
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 22:45:18 2024 +0100

    added missing toml req

commit 91b71ed24a
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 20:49:50 2024 +0100

    moved isort config

commit 12460b0419
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 20:43:22 2024 +0100

    remove flake8 section from setup.cfg

commit f5cf7b2e78
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 20:36:57 2024 +0100

    fixed reqs

commit 9d845bee98
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 20:32:35 2024 +0100

    disable autofix/format

commit aff5f27148
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 20:28:50 2024 +0100

    adjust checks

commit 47271cf1ef
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 20:28:22 2024 +0100

    reorder order of operations

commit e1bf178b40
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 20:01:09 2024 +0100

    adapted ruff settings to better fit code base

commit ad7d88a6f4
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 19:59:45 2024 +0100

    auto fixed docstring

commit a2e54a760e
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 19:46:35 2024 +0100

    fix getattr useage

commit cb80c73bc6
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 19:25:09 2024 +0100

    fix requirements file

commit b7780bbd21
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:42:28 2024 +0100

    fix removed sections

commit 71f1681f55
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:41:21 2024 +0100

    fix djlint syntax

commit a0bcf1bcce
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:35:28 2024 +0100

    remove flake8 from code base

commit 22475b31cc
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:34:56 2024 +0100

    remove flake8 from code base

commit 0413350f14
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:24:39 2024 +0100

    moved ruff section

commit d90c48a0bf
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:24:24 2024 +0100

    move djlint config to pyproject

commit c5ce55d511
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:20:39 2024 +0100

    added isort again

commit 42a41d23af
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:19:02 2024 +0100

    move config section

commit 8569233181
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:17:52 2024 +0100

    fix codespell error

commit 2897c6704d
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 17:29:21 2024 +0100

    replaced flake8 with ruff
    mostly for speed improvements

* fix coverage souce format

---------

Co-authored-by: Oliver Walters <oliver.henry.walters@gmail.com>
This commit is contained in:
Matthias Mair
2024-01-11 01:28:58 +01:00
committed by GitHub
parent 9715af564f
commit 4b14986591
257 changed files with 13422 additions and 12200 deletions

View File

@ -22,7 +22,7 @@ class SettingsAdmin(ImportExportModelAdmin):
class UserSettingsAdmin(ImportExportModelAdmin):
"""Admin settings for InvenTreeUserSetting."""
list_display = ('key', 'value', 'user', )
list_display = ('key', 'value', 'user')
def get_readonly_fields(self, request, obj=None): # pragma: no cover
"""Prevent the 'key' field being edited once the setting is created."""
@ -40,23 +40,31 @@ class WebhookAdmin(ImportExportModelAdmin):
class NotificationEntryAdmin(admin.ModelAdmin):
"""Admin settings for NotificationEntry."""
list_display = ('key', 'uid', 'updated', )
list_display = ('key', 'uid', 'updated')
class NotificationMessageAdmin(admin.ModelAdmin):
"""Admin settings for NotificationMessage."""
list_display = ('age_human', 'user', 'category', 'name', 'read', 'target_object', 'source_object', )
list_display = (
'age_human',
'user',
'category',
'name',
'read',
'target_object',
'source_object',
)
list_filter = ('category', 'read', 'user', )
list_filter = ('category', 'read', 'user')
search_fields = ('name', 'category', 'message', )
search_fields = ('name', 'category', 'message')
class NewsFeedEntryAdmin(admin.ModelAdmin):
"""Admin settings for NewsFeedEntry."""
list_display = ('title', 'author', 'published', 'summary', )
list_display = ('title', 'author', 'published', 'summary')
admin.site.register(common.models.InvenTreeSetting, SettingsAdmin)

View File

@ -23,8 +23,13 @@ from InvenTree.api import BulkDeleteMixin, MetadataView
from InvenTree.config import CONFIG_LOOKUPS
from InvenTree.filters import ORDER_FILTER, SEARCH_ORDER_FILTER
from InvenTree.helpers import inheritors
from InvenTree.mixins import (ListAPI, ListCreateAPI, RetrieveAPI,
RetrieveUpdateAPI, RetrieveUpdateDestroyAPI)
from InvenTree.mixins import (
ListAPI,
ListCreateAPI,
RetrieveAPI,
RetrieveUpdateAPI,
RetrieveUpdateDestroyAPI,
)
from InvenTree.permissions import IsStaffOrReadOnly, IsSuperuser
from plugin.models import NotificationUserSetting
from plugin.serializers import NotificationUserSettingSerializer
@ -41,6 +46,7 @@ class CsrfExemptMixin(object):
class WebhookView(CsrfExemptMixin, APIView):
"""Endpoint for receiving webhooks."""
authentication_classes = []
permission_classes = []
model_class = common.models.WebhookEndpoint
@ -66,8 +72,7 @@ class WebhookView(CsrfExemptMixin, APIView):
async_task(self._process_payload, message.id)
else:
self._process_result(
self.webhook.process_payload(message, payload, headers),
message,
self.webhook.process_payload(message, payload, headers), message
)
data = self.webhook.get_return(payload, headers, request)
@ -76,8 +81,7 @@ class WebhookView(CsrfExemptMixin, APIView):
def _process_payload(self, message_id):
message = common.models.WebhookMessage.objects.get(message_id=message_id)
self._process_result(
self.webhook.process_payload(message, message.body, message.header),
message,
self.webhook.process_payload(message, message.body, message.header), message
)
def _process_result(self, result, message):
@ -108,9 +112,7 @@ class WebhookView(CsrfExemptMixin, APIView):
class CurrencyExchangeView(APIView):
"""API endpoint for displaying currency information"""
permission_classes = [
permissions.IsAuthenticated,
]
permission_classes = [permissions.IsAuthenticated]
def get(self, request, format=None):
"""Return information on available currency conversions"""
@ -133,7 +135,9 @@ class CurrencyExchangeView(APIView):
updated = None
response = {
'base_currency': common.models.InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY', 'USD'),
'base_currency': common.models.InvenTreeSetting.get_setting(
'INVENTREE_DEFAULT_CURRENCY', 'USD'
),
'exchange_rates': {},
'updated': updated,
}
@ -150,10 +154,7 @@ class CurrencyRefreshView(APIView):
User must be a 'staff' user to access this endpoint
"""
permission_classes = [
permissions.IsAuthenticated,
permissions.IsAdminUser,
]
permission_classes = [permissions.IsAuthenticated, permissions.IsAdminUser]
def post(self, request, *args, **kwargs):
"""Performing a POST request will update currency exchange rates"""
@ -161,9 +162,7 @@ class CurrencyRefreshView(APIView):
update_exchange_rates(force=True)
return Response({
'success': 'Exchange rates updated',
})
return Response({'success': 'Exchange rates updated'})
class SettingsList(ListAPI):
@ -174,21 +173,15 @@ class SettingsList(ListAPI):
filter_backends = SEARCH_ORDER_FILTER
ordering_fields = [
'pk',
'key',
'name',
]
ordering_fields = ['pk', 'key', 'name']
search_fields = [
'key',
]
search_fields = ['key']
class GlobalSettingsList(SettingsList):
"""API endpoint for accessing a list of global settings objects."""
queryset = common.models.InvenTreeSetting.objects.exclude(key__startswith="_")
queryset = common.models.InvenTreeSetting.objects.exclude(key__startswith='_')
serializer_class = common.serializers.GlobalSettingsSerializer
def list(self, request, *args, **kwargs):
@ -221,25 +214,24 @@ class GlobalSettingsDetail(RetrieveUpdateAPI):
"""
lookup_field = 'key'
queryset = common.models.InvenTreeSetting.objects.exclude(key__startswith="_")
queryset = common.models.InvenTreeSetting.objects.exclude(key__startswith='_')
serializer_class = common.serializers.GlobalSettingsSerializer
def get_object(self):
"""Attempt to find a global setting object with the provided key."""
key = str(self.kwargs['key']).upper()
if key.startswith('_') or key not in common.models.InvenTreeSetting.SETTINGS.keys():
if (
key.startswith('_')
or key not in common.models.InvenTreeSetting.SETTINGS.keys()
):
raise NotFound()
return common.models.InvenTreeSetting.get_setting_object(
key,
cache=False, create=True
key, cache=False, create=True
)
permission_classes = [
permissions.IsAuthenticated,
GlobalSettingsPermissions,
]
permission_classes = [permissions.IsAuthenticated, GlobalSettingsPermissions]
class UserSettingsList(SettingsList):
@ -294,18 +286,17 @@ class UserSettingsDetail(RetrieveUpdateAPI):
"""Attempt to find a user setting object with the provided key."""
key = str(self.kwargs['key']).upper()
if key.startswith('_') or key not in common.models.InvenTreeUserSetting.SETTINGS.keys():
if (
key.startswith('_')
or key not in common.models.InvenTreeUserSetting.SETTINGS.keys()
):
raise NotFound()
return common.models.InvenTreeUserSetting.get_setting_object(
key,
user=self.request.user,
cache=False, create=True
key, user=self.request.user, cache=False, create=True
)
permission_classes = [
UserSettingsPermissions,
]
permission_classes = [UserSettingsPermissions]
class NotificationUserSettingsList(SettingsList):
@ -334,39 +325,29 @@ class NotificationUserSettingsDetail(RetrieveUpdateAPI):
queryset = NotificationUserSetting.objects.all()
serializer_class = NotificationUserSettingSerializer
permission_classes = [UserSettingsPermissions, ]
permission_classes = [UserSettingsPermissions]
class NotificationMessageMixin:
"""Generic mixin for NotificationMessage."""
queryset = common.models.NotificationMessage.objects.all()
serializer_class = common.serializers.NotificationMessageSerializer
permission_classes = [UserSettingsPermissions, ]
permission_classes = [UserSettingsPermissions]
class NotificationList(NotificationMessageMixin, BulkDeleteMixin, ListAPI):
"""List view for all notifications of the current user."""
permission_classes = [permissions.IsAuthenticated, ]
permission_classes = [permissions.IsAuthenticated]
filter_backends = SEARCH_ORDER_FILTER
ordering_fields = [
'category',
'name',
'read',
'creation',
]
ordering_fields = ['category', 'name', 'read', 'creation']
search_fields = [
'name',
'message',
]
search_fields = ['name', 'message']
filterset_fields = [
'category',
'read',
]
filterset_fields = ['category', 'read']
def filter_queryset(self, queryset):
"""Only list notifications which apply to the current user."""
@ -401,29 +382,27 @@ class NotificationReadAll(NotificationMessageMixin, RetrieveAPI):
self.queryset.filter(user=request.user, read=False).update(read=True)
return Response({'status': 'ok'})
except Exception as exc:
raise serializers.ValidationError(detail=serializers.as_serializer_error(exc))
raise serializers.ValidationError(
detail=serializers.as_serializer_error(exc)
)
class NewsFeedMixin:
"""Generic mixin for NewsFeedEntry."""
queryset = common.models.NewsFeedEntry.objects.all()
serializer_class = common.serializers.NewsFeedEntrySerializer
permission_classes = [IsAdminUser, ]
permission_classes = [IsAdminUser]
class NewsFeedEntryList(NewsFeedMixin, BulkDeleteMixin, ListAPI):
"""List view for all news items."""
filter_backends = ORDER_FILTER
ordering_fields = [
'published',
'author',
'read',
]
ordering_fields = ['published', 'author', 'read']
filterset_fields = [
'read',
]
filterset_fields = ['read']
class NewsFeedEntryDetail(NewsFeedMixin, RetrieveUpdateDestroyAPI):
@ -435,14 +414,14 @@ class ConfigList(ListAPI):
queryset = CONFIG_LOOKUPS
serializer_class = common.serializers.ConfigSerializer
permission_classes = [IsSuperuser, ]
permission_classes = [IsSuperuser]
class ConfigDetail(RetrieveAPI):
"""Detail view for an individual configuration."""
serializer_class = common.serializers.ConfigSerializer
permission_classes = [IsSuperuser, ]
permission_classes = [IsSuperuser]
def get_object(self):
"""Attempt to find a config object with the provided key."""
@ -458,7 +437,7 @@ class NotesImageList(ListCreateAPI):
queryset = common.models.NotesImage.objects.all()
serializer_class = common.serializers.NotesImageSerializer
permission_classes = [permissions.IsAuthenticated, ]
permission_classes = [permissions.IsAuthenticated]
def perform_create(self, serializer):
"""Create (upload) a new notes image"""
@ -475,14 +454,9 @@ class ProjectCodeList(ListCreateAPI):
permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly]
filter_backends = SEARCH_ORDER_FILTER
ordering_fields = [
'code',
]
ordering_fields = ['code']
search_fields = [
'code',
'description',
]
search_fields = ['code', 'description']
class ProjectCodeDetail(RetrieveUpdateDestroyAPI):
@ -515,14 +489,14 @@ class FlagList(ListAPI):
queryset = settings.FLAGS
serializer_class = common.serializers.FlagSerializer
permission_classes = [permissions.AllowAny, ]
permission_classes = [permissions.AllowAny]
class FlagDetail(RetrieveAPI):
"""Detail view for an individual feature flag."""
serializer_class = common.serializers.FlagSerializer
permission_classes = [permissions.AllowAny, ]
permission_classes = [permissions.AllowAny]
def get_object(self):
"""Attempt to find a config object with the provided key."""
@ -535,97 +509,175 @@ class FlagDetail(RetrieveAPI):
settings_api_urls = [
# User settings
re_path(r'^user/', include([
# User Settings Detail
re_path(r'^(?P<key>\w+)/', UserSettingsDetail.as_view(), name='api-user-setting-detail'),
# User Settings List
re_path(r'^.*$', UserSettingsList.as_view(), name='api-user-setting-list'),
])),
re_path(
r'^user/',
include([
# User Settings Detail
re_path(
r'^(?P<key>\w+)/',
UserSettingsDetail.as_view(),
name='api-user-setting-detail',
),
# User Settings List
re_path(r'^.*$', UserSettingsList.as_view(), name='api-user-setting-list'),
]),
),
# Notification settings
re_path(r'^notification/', include([
# Notification Settings Detail
path(r'<int:pk>/', NotificationUserSettingsDetail.as_view(), name='api-notification-setting-detail'),
# Notification Settings List
re_path(r'^.*$', NotificationUserSettingsList.as_view(), name='api-notification-setting-list'),
])),
re_path(
r'^notification/',
include([
# Notification Settings Detail
path(
r'<int:pk>/',
NotificationUserSettingsDetail.as_view(),
name='api-notification-setting-detail',
),
# Notification Settings List
re_path(
r'^.*$',
NotificationUserSettingsList.as_view(),
name='api-notification-setting-list',
),
]),
),
# Global settings
re_path(r'^global/', include([
# Global Settings Detail
re_path(r'^(?P<key>\w+)/', GlobalSettingsDetail.as_view(), name='api-global-setting-detail'),
# Global Settings List
re_path(r'^.*$', GlobalSettingsList.as_view(), name='api-global-setting-list'),
])),
re_path(
r'^global/',
include([
# Global Settings Detail
re_path(
r'^(?P<key>\w+)/',
GlobalSettingsDetail.as_view(),
name='api-global-setting-detail',
),
# Global Settings List
re_path(
r'^.*$', GlobalSettingsList.as_view(), name='api-global-setting-list'
),
]),
),
]
common_api_urls = [
# Webhooks
path('webhook/<slug:endpoint>/', WebhookView.as_view(), name='api-webhook'),
# Uploaded images for notes
re_path(r'^notes-image-upload/', NotesImageList.as_view(), name='api-notes-image-list'),
re_path(
r'^notes-image-upload/', NotesImageList.as_view(), name='api-notes-image-list'
),
# Project codes
re_path(r'^project-code/', include([
path(r'<int:pk>/', include([
re_path(r'^metadata/', MetadataView.as_view(), {'model': common.models.ProjectCode}, name='api-project-code-metadata'),
re_path(r'^.*$', ProjectCodeDetail.as_view(), name='api-project-code-detail'),
])),
re_path(r'^.*$', ProjectCodeList.as_view(), name='api-project-code-list'),
])),
re_path(
r'^project-code/',
include([
path(
r'<int:pk>/',
include([
re_path(
r'^metadata/',
MetadataView.as_view(),
{'model': common.models.ProjectCode},
name='api-project-code-metadata',
),
re_path(
r'^.*$',
ProjectCodeDetail.as_view(),
name='api-project-code-detail',
),
]),
),
re_path(r'^.*$', ProjectCodeList.as_view(), name='api-project-code-list'),
]),
),
# Custom physical units
re_path(r'^units/', include([
path(r'<int:pk>/', include([
re_path(r'^.*$', CustomUnitDetail.as_view(), name='api-custom-unit-detail'),
])),
re_path(r'^.*$', CustomUnitList.as_view(), name='api-custom-unit-list'),
])),
re_path(
r'^units/',
include([
path(
r'<int:pk>/',
include([
re_path(
r'^.*$',
CustomUnitDetail.as_view(),
name='api-custom-unit-detail',
)
]),
),
re_path(r'^.*$', CustomUnitList.as_view(), name='api-custom-unit-list'),
]),
),
# Currencies
re_path(r'^currency/', include([
re_path(r'^exchange/', CurrencyExchangeView.as_view(), name='api-currency-exchange'),
re_path(r'^refresh/', CurrencyRefreshView.as_view(), name='api-currency-refresh'),
])),
re_path(
r'^currency/',
include([
re_path(
r'^exchange/',
CurrencyExchangeView.as_view(),
name='api-currency-exchange',
),
re_path(
r'^refresh/', CurrencyRefreshView.as_view(), name='api-currency-refresh'
),
]),
),
# Notifications
re_path(r'^notifications/', include([
# Individual purchase order detail URLs
path(r'<int:pk>/', include([
re_path(r'.*$', NotificationDetail.as_view(), name='api-notifications-detail'),
])),
# Read all
re_path(r'^readall/', NotificationReadAll.as_view(), name='api-notifications-readall'),
# Notification messages list
re_path(r'^.*$', NotificationList.as_view(), name='api-notifications-list'),
])),
re_path(
r'^notifications/',
include([
# Individual purchase order detail URLs
path(
r'<int:pk>/',
include([
re_path(
r'.*$',
NotificationDetail.as_view(),
name='api-notifications-detail',
)
]),
),
# Read all
re_path(
r'^readall/',
NotificationReadAll.as_view(),
name='api-notifications-readall',
),
# Notification messages list
re_path(r'^.*$', NotificationList.as_view(), name='api-notifications-list'),
]),
),
# News
re_path(r'^news/', include([
path(r'<int:pk>/', include([
re_path(r'.*$', NewsFeedEntryDetail.as_view(), name='api-news-detail'),
])),
re_path(r'^.*$', NewsFeedEntryList.as_view(), name='api-news-list'),
])),
re_path(
r'^news/',
include([
path(
r'<int:pk>/',
include([
re_path(
r'.*$', NewsFeedEntryDetail.as_view(), name='api-news-detail'
)
]),
),
re_path(r'^.*$', NewsFeedEntryList.as_view(), name='api-news-list'),
]),
),
# Flags
path('flags/', include([
path('<str:key>/', FlagDetail.as_view(), name='api-flag-detail'),
re_path(r'^.*$', FlagList.as_view(), name='api-flag-list'),
])),
path(
'flags/',
include([
path('<str:key>/', FlagDetail.as_view(), name='api-flag-detail'),
re_path(r'^.*$', FlagList.as_view(), name='api-flag-list'),
]),
),
# Status
path('generic/status/', include([
path(f'<str:{StatusView.MODEL_REF}>/', include([
path('', StatusView.as_view(), name='api-status'),
])),
path('', AllStatusViews.as_view(), name='api-status-all'),
])),
path(
'generic/status/',
include([
path(
f'<str:{StatusView.MODEL_REF}>/',
include([path('', StatusView.as_view(), name='api-status')]),
),
path('', AllStatusViews.as_view(), name='api-status-all'),
]),
),
]
admin_api_urls = [

View File

@ -30,10 +30,14 @@ class CommonConfig(AppConfig):
try:
import common.models
if common.models.InvenTreeSetting.get_setting('SERVER_RESTART_REQUIRED', backup_value=False, create=False, cache=False):
logger.info("Clearing SERVER_RESTART_REQUIRED flag")
if common.models.InvenTreeSetting.get_setting(
'SERVER_RESTART_REQUIRED', backup_value=False, create=False, cache=False
):
logger.info('Clearing SERVER_RESTART_REQUIRED flag')
if not InvenTree.ready.isImportingData():
common.models.InvenTreeSetting.set_setting('SERVER_RESTART_REQUIRED', False, None)
common.models.InvenTreeSetting.set_setting(
'SERVER_RESTART_REQUIRED', False, None
)
except Exception:
pass

View File

@ -49,12 +49,12 @@ class FileManager:
ext = os.path.splitext(file.name)[-1].lower().replace('.', '')
try:
if ext in ['csv', 'tsv', ]:
if ext in ['csv', 'tsv']:
# These file formats need string decoding
raw_data = file.read().decode('utf-8')
# Reset stream position to beginning of file
file.seek(0)
elif ext in ['xls', 'xlsx', 'json', 'yaml', ]:
elif ext in ['xls', 'xlsx', 'json', 'yaml']:
raw_data = file.read()
# Reset stream position to beginning of file
file.seek(0)
@ -81,7 +81,12 @@ class FileManager:
def update_headers(self):
"""Update headers."""
self.HEADERS = self.REQUIRED_HEADERS + self.ITEM_MATCH_HEADERS + self.OPTIONAL_MATCH_HEADERS + self.OPTIONAL_HEADERS
self.HEADERS = (
self.REQUIRED_HEADERS
+ self.ITEM_MATCH_HEADERS
+ self.OPTIONAL_MATCH_HEADERS
+ self.OPTIONAL_HEADERS
)
def setup(self):
"""Setup headers should be overridden in usage to set the Different Headers."""
@ -149,15 +154,9 @@ class FileManager:
break
if not guess_exists:
headers.append({
'name': header,
'guess': guess
})
headers.append({'name': header, 'guess': guess})
else:
headers.append({
'name': header,
'guess': None
})
headers.append({'name': header, 'guess': None})
return headers
@ -180,7 +179,6 @@ class FileManager:
rows = []
for i in range(self.row_count()):
data = list(self.get_row_data(i))
# Is the row completely empty? Skip!
@ -203,10 +201,7 @@ class FileManager:
if empty:
continue
row = {
'data': data,
'index': i
}
row = {'data': data, 'index': i}
rows.append(row)

View File

@ -9,10 +9,7 @@ from .files import FileManager
class UploadFileForm(forms.Form):
"""Step 1 of FileManagementFormView."""
file = forms.FileField(
label=_('File'),
help_text=_('Select file to upload'),
)
file = forms.FileField(label=_('File'), help_text=_('Select file to upload'))
def __init__(self, *args, **kwargs):
"""Update label and help_text."""
@ -67,9 +64,7 @@ class MatchFieldForm(forms.Form):
self.fields[field_name] = forms.ChoiceField(
choices=[('', '-' * 10)] + headers_choices,
required=False,
widget=forms.Select(attrs={
'class': 'select fieldselect',
})
widget=forms.Select(attrs={'class': 'select fieldselect'}),
)
if col['guess']:
self.fields[field_name].initial = col['guess']
@ -107,7 +102,9 @@ class MatchItemForm(forms.Form):
field_name = col_guess.lower() + '-' + str(row['index'])
# check if field def was overridden
overriden_field = self.get_special_field(col_guess, row, file_manager)
overriden_field = self.get_special_field(
col_guess, row, file_manager
)
if overriden_field:
self.fields[field_name] = overriden_field
@ -117,23 +114,23 @@ class MatchItemForm(forms.Form):
value = row.get(col_guess.lower(), '')
# Set field input box
self.fields[field_name] = forms.CharField(
required=True,
initial=value,
required=True, initial=value
)
# Create item selection box
elif col_guess in file_manager.OPTIONAL_MATCH_HEADERS:
# Get item options
item_options = [(option.id, option) for option in row['match_options_' + col_guess]]
item_options = [
(option.id, option)
for option in row['match_options_' + col_guess]
]
# Get item match
item_match = row['match_' + col_guess]
# Set field select box
self.fields[field_name] = forms.ChoiceField(
choices=[('', '-' * 10)] + item_options,
required=False,
widget=forms.Select(attrs={
'class': 'select bomselect',
})
widget=forms.Select(attrs={'class': 'select bomselect'}),
)
# Update select box when match was found
if item_match:
@ -142,7 +139,9 @@ class MatchItemForm(forms.Form):
# Create item selection box
elif col_guess in file_manager.ITEM_MATCH_HEADERS:
# Get item options
item_options = [(option.id, option) for option in row['item_options']]
item_options = [
(option.id, option) for option in row['item_options']
]
# Get item match
item_match = row['item_match']
# Set field name
@ -151,9 +150,7 @@ class MatchItemForm(forms.Form):
self.fields[field_name] = forms.ChoiceField(
choices=[('', '-' * 10)] + item_options,
required=False,
widget=forms.Select(attrs={
'class': 'select bomselect',
})
widget=forms.Select(attrs={'class': 'select bomselect'}),
)
# Update select box when match was found
if item_match:
@ -169,8 +166,7 @@ class MatchItemForm(forms.Form):
value = row.get(col_guess.lower(), '')
# Set field input box
self.fields[field_name] = forms.CharField(
required=False,
initial=value,
required=False, initial=value
)
def get_special_field(self, col_guess, row, file_manager):

File diff suppressed because it is too large Load Diff

View File

@ -24,7 +24,7 @@ class NotificationMethod:
METHOD_NAME = ''
METHOD_ICON = None
CONTEXT_BUILTIN = ['name', 'message', ]
CONTEXT_BUILTIN = ['name', 'message']
CONTEXT_EXTRA = []
GLOBAL_SETTING = None
USER_SETTING = None
@ -39,11 +39,15 @@ class NotificationMethod:
"""
# Check if a sending fnc is defined
if (not hasattr(self, 'send')) and (not hasattr(self, 'send_bulk')):
raise NotImplementedError('A NotificationMethod must either define a `send` or a `send_bulk` method')
raise NotImplementedError(
'A NotificationMethod must either define a `send` or a `send_bulk` method'
)
# No method name is no good
if self.METHOD_NAME in ('', None):
raise NotImplementedError(f'The NotificationMethod {self.__class__} did not provide a METHOD_NAME')
raise NotImplementedError(
f'The NotificationMethod {self.__class__} did not provide a METHOD_NAME'
)
# Check if plugin is disabled - if so do not gather targets etc.
if self.global_setting_disable():
@ -61,9 +65,10 @@ class NotificationMethod:
def check_context(self, context):
"""Check that all values defined in the methods CONTEXT were provided in the current context."""
def check(ref, obj):
# the obj is not accessible so we are on the end
if not isinstance(obj, (list, dict, tuple, )):
if not isinstance(obj, (list, dict, tuple)):
return ref
# check if the ref exists
@ -82,7 +87,9 @@ class NotificationMethod:
return check(ref[1:], obj[ref[0]])
# other cases -> raise
raise NotImplementedError('This type can not be used as a context reference')
raise NotImplementedError(
'This type can not be used as a context reference'
)
missing = []
for item in (*self.CONTEXT_BUILTIN, *self.CONTEXT_EXTRA):
@ -91,7 +98,9 @@ class NotificationMethod:
missing.append(ret)
if missing:
raise NotImplementedError(f'The `context` is missing the following items:\n{missing}')
raise NotImplementedError(
f'The `context` is missing the following items:\n{missing}'
)
return context
@ -142,7 +151,12 @@ class NotificationMethod:
def usersetting(self, target):
"""Returns setting for this method for a given user."""
return NotificationUserSetting.get_setting(f'NOTIFICATION_METHOD_{self.METHOD_NAME.upper()}', user=target, method=self.METHOD_NAME)
return NotificationUserSetting.get_setting(
f'NOTIFICATION_METHOD_{self.METHOD_NAME.upper()}',
user=target,
method=self.METHOD_NAME,
)
# endregion
@ -160,6 +174,8 @@ class BulkNotificationMethod(NotificationMethod):
def send_bulk(self):
"""This function must be overridden."""
raise NotImplementedError('The `send` method must be overridden!')
# endregion
@ -181,17 +197,25 @@ class MethodStorageClass:
selected_classes (class, optional): References to the classes that should be registered. Defaults to None.
"""
logger.debug('Collecting notification methods')
current_method = InvenTree.helpers.inheritors(NotificationMethod) - IGNORED_NOTIFICATION_CLS
current_method = (
InvenTree.helpers.inheritors(NotificationMethod) - IGNORED_NOTIFICATION_CLS
)
# for testing selective loading is made available
if selected_classes:
current_method = [item for item in current_method if item is selected_classes]
current_method = [
item for item in current_method if item is selected_classes
]
# make sure only one of each method is added
filtered_list = {}
for item in current_method:
plugin = item.get_plugin(item)
ref = f'{plugin.package_path}_{item.METHOD_NAME}' if plugin else item.METHOD_NAME
ref = (
f'{plugin.package_path}_{item.METHOD_NAME}'
if plugin
else item.METHOD_NAME
)
item.plugin = plugin() if plugin else None
filtered_list[ref] = item
@ -217,9 +241,7 @@ class MethodStorageClass:
# make sure the setting exists
self.user_settings[new_key] = item.USER_SETTING
NotificationUserSetting.get_setting(
key=new_key,
user=user,
method=item.METHOD_NAME,
key=new_key, user=user, method=item.METHOD_NAME
)
# save definition
@ -231,7 +253,7 @@ class MethodStorageClass:
return methods
IGNORED_NOTIFICATION_CLS = {SingleNotificationMethod, BulkNotificationMethod, }
IGNORED_NOTIFICATION_CLS = {SingleNotificationMethod, BulkNotificationMethod}
storage = MethodStorageClass()
@ -275,6 +297,7 @@ class NotificationBody:
app_label: App label (slugified) of the model
model_name': Name (slugified) of the model
"""
name: str
slug: str
message: str
@ -286,24 +309,25 @@ class InvenTreeNotificationBodies:
Contains regularly used notification bodies.
"""
NewOrder = NotificationBody(
name=_("New {verbose_name}"),
name=_('New {verbose_name}'),
slug='{app_label}.new_{model_name}',
message=_("A new order has been created and assigned to you"),
message=_('A new order has been created and assigned to you'),
template='email/new_order_assigned.html',
)
"""Send when a new order (build, sale or purchase) was created."""
OrderCanceled = NotificationBody(
name=_("{verbose_name} canceled"),
name=_('{verbose_name} canceled'),
slug='{app_label}.canceled_{model_name}',
message=_("A order that is assigned to you was canceled"),
message=_('A order that is assigned to you was canceled'),
template='email/canceled_order_assigned.html',
)
"""Send when a order (sale, return or purchase) was canceled."""
ItemsReceived = NotificationBody(
name=_("Items Received"),
name=_('Items Received'),
slug='purchase_order.items_received',
message=_('Items have been received against a purchase order'),
template='email/purchase_order_received.html',
@ -340,13 +364,19 @@ def trigger_notification(obj, category=None, obj_ref='pk', **kwargs):
if not obj_ref_value:
obj_ref_value = getattr(obj, 'id', None)
if not obj_ref_value:
raise KeyError(f"Could not resolve an object reference for '{str(obj)}' with {obj_ref}, pk, id")
raise KeyError(
f"Could not resolve an object reference for '{str(obj)}' with {obj_ref}, pk, id"
)
# Check if we have notified recently...
delta = timedelta(days=1)
if common.models.NotificationEntry.check_recent(category, obj_ref_value, delta):
logger.info("Notification '%s' has recently been sent for '%s' - SKIPPING", category, str(obj))
logger.info(
"Notification '%s' has recently been sent for '%s' - SKIPPING",
category,
str(obj),
)
return
logger.info("Gathering users for notification '%s'", category)
@ -383,7 +413,9 @@ def trigger_notification(obj, category=None, obj_ref='pk', **kwargs):
target_users.add(user)
# Unhandled type
else:
logger.error("Unknown target passed to trigger_notification method: %s", target)
logger.error(
'Unknown target passed to trigger_notification method: %s', target
)
if target_users:
logger.info("Sending notification '%s' for '%s'", category, str(obj))
@ -392,7 +424,7 @@ def trigger_notification(obj, category=None, obj_ref='pk', **kwargs):
if delivery_methods is None:
delivery_methods = storage.liste
else:
delivery_methods = (delivery_methods - IGNORED_NOTIFICATION_CLS)
delivery_methods = delivery_methods - IGNORED_NOTIFICATION_CLS
for method in delivery_methods:
logger.info("Triggering notification method '%s'", method.METHOD_NAME)
@ -422,17 +454,15 @@ def trigger_superuser_notification(plugin: PluginConfig, msg: str):
trigger_notification(
plugin,
'inventree.plugin',
context={
'error': plugin,
'name': _('Error raised by plugin'),
'message': msg,
},
context={'error': plugin, 'name': _('Error raised by plugin'), 'message': msg},
targets=users,
delivery_methods={UIMessageNotification, },
delivery_methods={UIMessageNotification},
)
def deliver_notification(cls: NotificationMethod, obj, category: str, targets, context: dict):
def deliver_notification(
cls: NotificationMethod, obj, category: str, targets, context: dict
):
"""Send notification with the provided class.
This:
@ -447,7 +477,12 @@ def deliver_notification(cls: NotificationMethod, obj, category: str, targets, c
if method.targets and len(method.targets) > 0:
# Log start
logger.info("Notify users via '%s' for notification '%s' for '%s'", method.METHOD_NAME, category, str(obj))
logger.info(
"Notify users via '%s' for notification '%s' for '%s'",
method.METHOD_NAME,
category,
str(obj),
)
# Run setup for delivery method
method.setup()
@ -472,6 +507,12 @@ def deliver_notification(cls: NotificationMethod, obj, category: str, targets, c
method.cleanup()
# Log results
logger.info("Notified %s users via '%s' for notification '%s' for '%s' successfully", success_count, method.METHOD_NAME, category, str(obj))
logger.info(
"Notified %s users via '%s' for notification '%s' for '%s' successfully",
success_count,
method.METHOD_NAME,
category,
str(obj),
)
if not success:
logger.info("There were some problems")
logger.info('There were some problems')

View File

@ -1,6 +1,5 @@
"""JSON serializers for common components."""
from django.urls import reverse
from flags.state import flag_state
@ -9,8 +8,10 @@ from rest_framework import serializers
import common.models as common_models
from InvenTree.helpers import get_objectreference
from InvenTree.helpers_model import construct_absolute_url
from InvenTree.serializers import (InvenTreeImageSerializerField,
InvenTreeModelSerializer)
from InvenTree.serializers import (
InvenTreeImageSerializerField,
InvenTreeModelSerializer,
)
from users.serializers import OwnerSerializer
@ -62,10 +63,7 @@ class SettingsSerializer(InvenTreeModelSerializer):
if choices:
for choice in choices:
results.append({
'value': choice[0],
'display_name': choice[1],
})
results.append({'value': choice[0], 'display_name': choice[1]})
return results
@ -131,8 +129,10 @@ class GenericReferencedSettingSerializer(SettingsSerializer):
def __init__(self, *args, **kwargs):
"""Init overrides the Meta class to make it dynamic."""
class CustomMeta:
"""Scaffold for custom Meta class."""
fields = [
'pk',
'key',
@ -204,10 +204,12 @@ class NotificationMessageSerializer(InvenTreeModelSerializer):
request = self.context['request']
if request.user and request.user.is_staff:
meta = obj.target_object._meta
target['link'] = construct_absolute_url(reverse(
f'admin:{meta.db_table}_change',
kwargs={'object_id': obj.target_object_id}
))
target['link'] = construct_absolute_url(
reverse(
f'admin:{meta.db_table}_change',
kwargs={'object_id': obj.target_object_id},
)
)
return target
@ -257,17 +259,9 @@ class NotesImageSerializer(InvenTreeModelSerializer):
"""Meta options for NotesImageSerializer."""
model = common_models.NotesImage
fields = [
'pk',
'image',
'user',
'date',
]
fields = ['pk', 'image', 'user', 'date']
read_only_fields = [
'date',
'user',
]
read_only_fields = ['date', 'user']
image = InvenTreeImageSerializerField(required=True)
@ -279,13 +273,7 @@ class ProjectCodeSerializer(InvenTreeModelSerializer):
"""Meta options for ProjectCodeSerializer."""
model = common_models.ProjectCode
fields = [
'pk',
'code',
'description',
'responsible',
'responsible_detail',
]
fields = ['pk', 'code', 'description', 'responsible', 'responsible_detail']
responsible_detail = OwnerSerializer(source='responsible', read_only=True)
@ -313,9 +301,4 @@ class CustomUnitSerializer(InvenTreeModelSerializer):
"""Meta options for CustomUnitSerializer."""
model = common_models.CustomUnit
fields = [
'pk',
'name',
'symbol',
'definition',
]
fields = ['pk', 'name', 'symbol', 'definition']

View File

@ -20,7 +20,9 @@ def currency_code_default():
return cached_value
try:
code = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY', backup_value='', create=True, cache=True)
code = InvenTreeSetting.get_setting(
'INVENTREE_DEFAULT_CURRENCY', backup_value='', create=True, cache=True
)
except Exception: # pragma: no cover
# Database may not yet be ready, no need to throw an error here
code = ''

View File

@ -27,7 +27,9 @@ def delete_old_notifications():
try:
from common.models import NotificationEntry
except AppRegistryNotReady: # pragma: no cover
logger.info("Could not perform 'delete_old_notifications' - App registry not ready")
logger.info(
"Could not perform 'delete_old_notifications' - App registry not ready"
)
return
before = timezone.now() - timedelta(days=90)
@ -49,7 +51,7 @@ def update_news_feed():
try:
d = feedparser.parse(settings.INVENTREE_NEWS_URL)
except Exception as entry: # pragma: no cover
logger.warning("update_news_feed: Error parsing the newsfeed", entry)
logger.warning('update_news_feed: Error parsing the newsfeed', entry)
return
# Get a reference list
@ -87,13 +89,15 @@ def delete_old_notes_images():
try:
from common.models import NotesImage
except AppRegistryNotReady:
logger.info("Could not perform 'delete_old_notes_images' - App registry not ready")
logger.info(
"Could not perform 'delete_old_notes_images' - App registry not ready"
)
return
# Remove any notes which point to non-existent image files
for note in NotesImage.objects.all():
if not os.path.exists(note.image.path):
logger.info("Deleting note %s - image file does not exist", note.image.path)
logger.info('Deleting note %s - image file does not exist', note.image.path)
note.delete()
note_classes = getModelsWithMixin(InvenTreeNotesMixin)
@ -112,7 +116,7 @@ def delete_old_notes_images():
break
if not found:
logger.info("Deleting note %s - image file not linked to a note", img)
logger.info('Deleting note %s - image file not linked to a note', img)
note.delete()
# Finally, remove any images in the notes dir which are not linked to a note
@ -127,7 +131,6 @@ def delete_old_notes_images():
all_notes = NotesImage.objects.all()
for image in images:
found = False
for note in all_notes:
img_path = os.path.basename(note.image.path)
@ -136,5 +139,5 @@ def delete_old_notes_images():
break
if not found:
logger.info("Deleting note %s - image file not linked to a note", image)
logger.info('Deleting note %s - image file not linked to a note', image)
os.remove(os.path.join(notes_dir, image))

View File

@ -1,8 +1,12 @@
"""Tests for basic notification methods and functions in InvenTree."""
import plugin.templatetags.plugin_extras as plugin_tags
from common.notifications import (BulkNotificationMethod, NotificationMethod,
SingleNotificationMethod, storage)
from common.notifications import (
BulkNotificationMethod,
NotificationMethod,
SingleNotificationMethod,
storage,
)
from part.test_part import BaseNotificationIntegrationTest
from plugin.models import NotificationUserSetting
@ -23,37 +27,31 @@ class BaseNotificationTests(BaseNotificationIntegrationTest):
"""A comment so we do not need a pass."""
class NoNameNotificationMethod(NotificationMethod):
def send(self):
"""A comment so we do not need a pass."""
class WrongContextNotificationMethod(NotificationMethod):
METHOD_NAME = 'WrongContextNotification'
CONTEXT_EXTRA = [
'aa',
('aa', 'bb', ),
('templates', 'ccc', ),
(123, )
]
CONTEXT_EXTRA = ['aa', ('aa', 'bb'), ('templates', 'ccc'), (123,)]
def send(self):
"""A comment so we do not need a pass."""
# no send / send bulk
with self.assertRaises(NotImplementedError):
FalseNotificationMethod('', '', '', '', )
FalseNotificationMethod('', '', '', '')
# no METHOD_NAME
with self.assertRaises(NotImplementedError):
NoNameNotificationMethod('', '', '', '', )
NoNameNotificationMethod('', '', '', '')
# a not existent context check
with self.assertRaises(NotImplementedError):
WrongContextNotificationMethod('', '', '', '', )
WrongContextNotificationMethod('', '', '', '')
# no get_targets
with self.assertRaises(NotImplementedError):
AnotherFalseNotificationMethod('', '', '', {'name': 1, 'message': 2, }, )
AnotherFalseNotificationMethod('', '', '', {'name': 1, 'message': 2})
def test_failing_passing(self):
"""Ensure that an error in one deliverymethod is not blocking all mehthods."""
@ -67,7 +65,7 @@ class BaseNotificationTests(BaseNotificationIntegrationTest):
METHOD_NAME = 'ErrorImplementation'
def get_targets(self):
return [1, ]
return [1]
def send(self, target):
raise KeyError('This could be any error')
@ -91,7 +89,7 @@ class BulkNotificationMethodTests(BaseNotificationIntegrationTest):
METHOD_NAME = 'WrongImplementationBulk'
def get_targets(self):
return [1, ]
return [1]
with self.assertLogs(logger='inventree', level='ERROR'):
self._notification_run(WrongImplementation)
@ -113,11 +111,12 @@ class SingleNotificationMethodTests(BaseNotificationIntegrationTest):
METHOD_NAME = 'WrongImplementationSingle'
def get_targets(self):
return [1, ]
return [1]
with self.assertLogs(logger='inventree', level='ERROR'):
self._notification_run(WrongImplementation)
# A integration test for notifications is provided in test_part.PartNotificationTest
@ -144,7 +143,7 @@ class NotificationUserSettingTests(BaseNotificationIntegrationTest):
}
def get_targets(self):
return [1, ]
return [1]
def send_bulk(self):
return True
@ -158,10 +157,14 @@ class NotificationUserSettingTests(BaseNotificationIntegrationTest):
# assertions for settings
self.assertEqual(setting.name, 'Enable test notifications')
self.assertEqual(setting.default_value, True)
self.assertEqual(setting.description, 'Allow sending of test for event notifications')
self.assertEqual(
setting.description, 'Allow sending of test for event notifications'
)
self.assertEqual(setting.units, 'alpha')
# test tag and array
self.assertEqual(plugin_tags.notification_settings_list({'user': self.user}), array)
self.assertEqual(
plugin_tags.notification_settings_list({'user': self.user}), array
)
self.assertEqual(array[0]['key'], 'NOTIFICATION_METHOD_TEST')
self.assertEqual(array[0]['method'], 'test')

View File

@ -15,4 +15,4 @@ class TaskTest(TestCase):
"""Test that the task `delete_old_notifications` runs through without errors."""
# check empty run
self.assertEqual(NotificationEntry.objects.all().count(), 0)
offload_task(common_tasks.delete_old_notifications,)
offload_task(common_tasks.delete_old_notifications)

View File

@ -17,16 +17,23 @@ from django.urls import reverse
import PIL
from InvenTree.helpers import str2bool
from InvenTree.unit_test import (InvenTreeAPITestCase, InvenTreeTestCase,
PluginMixin)
from InvenTree.unit_test import InvenTreeAPITestCase, InvenTreeTestCase, PluginMixin
from plugin import registry
from plugin.models import NotificationUserSetting
from .api import WebhookView
from .models import (ColorTheme, CustomUnit, InvenTreeSetting,
InvenTreeUserSetting, NotesImage, NotificationEntry,
NotificationMessage, ProjectCode, WebhookEndpoint,
WebhookMessage)
from .models import (
ColorTheme,
CustomUnit,
InvenTreeSetting,
InvenTreeUserSetting,
NotesImage,
NotificationEntry,
NotificationMessage,
ProjectCode,
WebhookEndpoint,
WebhookMessage,
)
CONTENT_TYPE_JSON = 'application/json'
@ -34,9 +41,7 @@ CONTENT_TYPE_JSON = 'application/json'
class SettingsTest(InvenTreeTestCase):
"""Tests for the 'settings' model."""
fixtures = [
'settings',
]
fixtures = ['settings']
def test_settings_objects(self):
"""Test fixture loading and lookup for settings."""
@ -50,7 +55,9 @@ class SettingsTest(InvenTreeTestCase):
self.assertEqual(instance_name.value, 'My very first InvenTree Instance')
# Check object lookup (case insensitive)
self.assertEqual(InvenTreeSetting.get_setting_object('iNvEnTrEE_inSTanCE').pk, 1)
self.assertEqual(
InvenTreeSetting.get_setting_object('iNvEnTrEE_inSTanCE').pk, 1
)
def test_settings_functions(self):
"""Test settings functions and properties."""
@ -61,14 +68,25 @@ class SettingsTest(InvenTreeTestCase):
stale_ref = 'STOCK_STALE_DAYS'
stale_days = InvenTreeSetting.get_setting_object(stale_ref, cache=False)
report_size_obj = InvenTreeSetting.get_setting_object('REPORT_DEFAULT_PAGE_SIZE')
report_test_obj = InvenTreeSetting.get_setting_object('REPORT_ENABLE_TEST_REPORT')
report_size_obj = InvenTreeSetting.get_setting_object(
'REPORT_DEFAULT_PAGE_SIZE'
)
report_test_obj = InvenTreeSetting.get_setting_object(
'REPORT_ENABLE_TEST_REPORT'
)
# check settings base fields
self.assertEqual(instance_obj.name, 'Server Instance Name')
self.assertEqual(instance_obj.get_setting_name(instance_ref), 'Server Instance Name')
self.assertEqual(instance_obj.description, 'String descriptor for the server instance')
self.assertEqual(instance_obj.get_setting_description(instance_ref), 'String descriptor for the server instance')
self.assertEqual(
instance_obj.get_setting_name(instance_ref), 'Server Instance Name'
)
self.assertEqual(
instance_obj.description, 'String descriptor for the server instance'
)
self.assertEqual(
instance_obj.get_setting_description(instance_ref),
'String descriptor for the server instance',
)
# check units
self.assertEqual(instance_obj.units, '')
@ -90,7 +108,9 @@ class SettingsTest(InvenTreeTestCase):
# check as_int
self.assertEqual(stale_days.as_int(), 0)
self.assertEqual(instance_obj.as_int(), 'InvenTree') # not an int -> return default
self.assertEqual(
instance_obj.as_int(), 'InvenTree'
) # not an int -> return default
# check as_bool
self.assertEqual(report_test_obj.as_bool(), True)
@ -116,62 +136,66 @@ class SettingsTest(InvenTreeTestCase):
def test_all_settings(self):
"""Make sure that the all_settings function returns correctly"""
result = InvenTreeSetting.all_settings()
self.assertIn("INVENTREE_INSTANCE", result)
self.assertIn('INVENTREE_INSTANCE', result)
self.assertIsInstance(result['INVENTREE_INSTANCE'], InvenTreeSetting)
@mock.patch("common.models.InvenTreeSetting.get_setting_definition")
@mock.patch('common.models.InvenTreeSetting.get_setting_definition')
def test_check_all_settings(self, get_setting_definition):
"""Make sure that the check_all_settings function returns correctly"""
# define partial schema
settings_definition = {
"AB": { # key that's has not already been accessed
"required": True,
'AB': { # key that's has not already been accessed
'required': True
},
"CD": {
"required": True,
"protected": True,
},
"EF": {}
'CD': {'required': True, 'protected': True},
'EF': {},
}
def mocked(key, **kwargs):
return settings_definition.get(key, {})
get_setting_definition.side_effect = mocked
self.assertEqual(InvenTreeSetting.check_all_settings(settings_definition=settings_definition), (False, ["AB", "CD"]))
InvenTreeSetting.set_setting('AB', "hello", self.user)
InvenTreeSetting.set_setting('CD', "world", self.user)
self.assertEqual(
InvenTreeSetting.check_all_settings(
settings_definition=settings_definition
),
(False, ['AB', 'CD']),
)
InvenTreeSetting.set_setting('AB', 'hello', self.user)
InvenTreeSetting.set_setting('CD', 'world', self.user)
self.assertEqual(InvenTreeSetting.check_all_settings(), (True, []))
@mock.patch("common.models.InvenTreeSetting.get_setting_definition")
@mock.patch('common.models.InvenTreeSetting.get_setting_definition')
def test_settings_validator(self, get_setting_definition):
"""Make sure that the validator function gets called on set setting."""
def validator(x):
if x == "hello":
if x == 'hello':
return x
raise ValidationError(f"{x} is not valid")
raise ValidationError(f'{x} is not valid')
mock_validator = mock.Mock(side_effect=validator)
# define partial schema
settings_definition = {
"AB": { # key that's has not already been accessed
"validator": mock_validator,
},
'AB': { # key that's has not already been accessed
'validator': mock_validator
}
}
def mocked(key, **kwargs):
return settings_definition.get(key, {})
get_setting_definition.side_effect = mocked
InvenTreeSetting.set_setting("AB", "hello", self.user)
mock_validator.assert_called_with("hello")
InvenTreeSetting.set_setting('AB', 'hello', self.user)
mock_validator.assert_called_with('hello')
with self.assertRaises(ValidationError):
InvenTreeSetting.set_setting("AB", "world", self.user)
mock_validator.assert_called_with("world")
InvenTreeSetting.set_setting('AB', 'world', self.user)
mock_validator.assert_called_with('world')
def run_settings_check(self, key, setting):
"""Test that all settings are valid.
@ -194,7 +218,9 @@ class SettingsTest(InvenTreeTestCase):
self.assertIn('django.utils.functional.lazy', str(type(description)))
if key != key.upper():
raise ValueError(f"Setting key '{key}' is not uppercase") # pragma: no cover
raise ValueError(
f"Setting key '{key}' is not uppercase"
) # pragma: no cover
# Check that only allowed keys are provided
allowed_keys = [
@ -232,7 +258,6 @@ class SettingsTest(InvenTreeTestCase):
- Ensure that every setting has a description, which is translated
"""
for key, setting in InvenTreeSetting.SETTINGS.items():
try:
self.run_settings_check(key, setting)
except Exception as exc: # pragma: no cover
@ -249,7 +274,6 @@ class SettingsTest(InvenTreeTestCase):
def test_defaults(self):
"""Populate the settings with default values."""
for key in InvenTreeSetting.SETTINGS.keys():
value = InvenTreeSetting.get_setting_default(key)
InvenTreeSetting.set_setting(key, value, self.user)
@ -261,10 +285,14 @@ class SettingsTest(InvenTreeTestCase):
if setting.is_bool():
if setting.default_value in ['', None]:
raise ValueError(f'Default value for boolean setting {key} not provided') # pragma: no cover
raise ValueError(
f'Default value for boolean setting {key} not provided'
) # pragma: no cover
if setting.default_value not in [True, False]:
raise ValueError(f'Non-boolean default value specified for {key}') # pragma: no cover
raise ValueError(
f'Non-boolean default value specified for {key}'
) # pragma: no cover
def test_global_setting_caching(self):
"""Test caching operations for the global settings class"""
@ -294,9 +322,7 @@ class SettingsTest(InvenTreeTestCase):
# Generate a number of new users
for idx in range(5):
get_user_model().objects.create(
username=f"User_{idx}",
password="hunter42",
email="email@dot.com",
username=f'User_{idx}', password='hunter42', email='email@dot.com'
)
key = 'SEARCH_PREVIEW_RESULTS'
@ -305,7 +331,10 @@ class SettingsTest(InvenTreeTestCase):
for user in get_user_model().objects.all():
setting = InvenTreeUserSetting.get_setting_object(key, user=user)
cache_key = setting.cache_key
self.assertEqual(cache_key, f"InvenTreeUserSetting:SEARCH_PREVIEW_RESULTS_user:{user.username}")
self.assertEqual(
cache_key,
f'InvenTreeUserSetting:SEARCH_PREVIEW_RESULTS_user:{user.username}',
)
InvenTreeUserSetting.set_setting(key, user.pk, None, user=user)
self.assertIsNotNone(cache.get(cache_key))
@ -333,7 +362,9 @@ class GlobalSettingsApiTest(InvenTreeAPITestCase):
response = self.get(url, expected_code=200)
n_public_settings = len([k for k in InvenTreeSetting.SETTINGS.keys() if not k.startswith('_')])
n_public_settings = len([
k for k in InvenTreeSetting.SETTINGS.keys() if not k.startswith('_')
])
# Number of results should match the number of settings
self.assertEqual(len(response.data), n_public_settings)
@ -358,13 +389,7 @@ class GlobalSettingsApiTest(InvenTreeAPITestCase):
# Test setting via the API
for val in ['cat', 'hat', 'bat', 'mat']:
response = self.patch(
url,
{
'value': val,
},
expected_code=200
)
response = self.patch(url, {'value': val}, expected_code=200)
self.assertEqual(response.data['value'], val)
@ -374,7 +399,7 @@ class GlobalSettingsApiTest(InvenTreeAPITestCase):
def test_api_detail(self):
"""Test that we can access the detail view for a setting based on the <key>."""
# These keys are invalid, and should return 404
for key in ["apple", "carrot", "dog"]:
for key in ['apple', 'carrot', 'dog']:
response = self.get(
reverse('api-global-setting-detail', kwargs={'key': key}),
expected_code=404,
@ -394,13 +419,7 @@ class GlobalSettingsApiTest(InvenTreeAPITestCase):
self.assertEqual(response.data['value'], 'InvenTree')
# Now, the object should have been created in the DB
self.patch(
url,
{
'value': 'My new title',
},
expected_code=200,
)
self.patch(url, {'value': 'My new title'}, expected_code=200)
setting = InvenTreeSetting.objects.get(key=key)
@ -451,8 +470,7 @@ class UserSettingsApiTest(InvenTreeAPITestCase):
"""Test a boolean user setting value."""
# Ensure we have a boolean setting available
setting = InvenTreeUserSetting.get_setting_object(
'SEARCH_PREVIEW_SHOW_PARTS',
user=self.user
'SEARCH_PREVIEW_SHOW_PARTS', user=self.user
)
# Check default values
@ -465,20 +483,16 @@ class UserSettingsApiTest(InvenTreeAPITestCase):
self.assertEqual(response.data['pk'], setting.pk)
self.assertEqual(response.data['key'], 'SEARCH_PREVIEW_SHOW_PARTS')
self.assertEqual(response.data['description'], 'Display parts in search preview window')
self.assertEqual(
response.data['description'], 'Display parts in search preview window'
)
self.assertEqual(response.data['type'], 'boolean')
self.assertEqual(len(response.data['choices']), 0)
self.assertTrue(str2bool(response.data['value']))
# Assign some truthy values
for v in ['true', True, 1, 'y', 'TRUE']:
self.patch(
url,
{
'value': str(v),
},
expected_code=200,
)
self.patch(url, {'value': str(v)}, expected_code=200)
response = self.get(url, expected_code=200)
@ -486,13 +500,7 @@ class UserSettingsApiTest(InvenTreeAPITestCase):
# Assign some false(ish) values
for v in ['false', False, '0', 'n', 'FalSe']:
self.patch(
url,
{
'value': str(v),
},
expected_code=200,
)
self.patch(url, {'value': str(v)}, expected_code=200)
response = self.get(url, expected_code=200)
@ -500,13 +508,7 @@ class UserSettingsApiTest(InvenTreeAPITestCase):
# Assign some invalid values
for v in ['x', '', 'invalid', None, '-1', 'abcde']:
response = self.patch(
url,
{
'value': str(v),
},
expected_code=200
)
response = self.patch(url, {'value': str(v)}, expected_code=200)
# Invalid values evaluate to False
self.assertFalse(str2bool(response.data['value']))
@ -514,8 +516,7 @@ class UserSettingsApiTest(InvenTreeAPITestCase):
def test_user_setting_choice(self):
"""Test a user setting with choices."""
setting = InvenTreeUserSetting.get_setting_object(
'DATE_DISPLAY_FORMAT',
user=self.user
'DATE_DISPLAY_FORMAT', user=self.user
)
url = reverse('api-user-setting-detail', kwargs={'key': setting.key})
@ -525,37 +526,21 @@ class UserSettingsApiTest(InvenTreeAPITestCase):
# Check that a valid option can be assigned via the API
for opt in ['YYYY-MM-DD', 'DD-MM-YYYY', 'MM/DD/YYYY']:
self.patch(
url,
{
'value': opt,
},
expected_code=200,
)
self.patch(url, {'value': opt}, expected_code=200)
setting.refresh_from_db()
self.assertEqual(setting.value, opt)
# Send an invalid option
for opt in ['cat', 'dog', 12345]:
response = self.patch(
url,
{
'value': opt,
},
expected_code=400,
)
response = self.patch(url, {'value': opt}, expected_code=400)
self.assertIn('Chosen value is not a valid option', str(response.data))
def test_user_setting_integer(self):
"""Test a integer user setting value."""
setting = InvenTreeUserSetting.get_setting_object(
'SEARCH_PREVIEW_RESULTS',
user=self.user,
cache=False,
'SEARCH_PREVIEW_RESULTS', user=self.user, cache=False
)
url = reverse('api-user-setting-detail', kwargs={'key': setting.key})
@ -573,13 +558,7 @@ class UserSettingsApiTest(InvenTreeAPITestCase):
# Set valid options via the api
for v in [5, 15, 25]:
self.patch(
url,
{
'value': v,
},
expected_code=200,
)
self.patch(url, {'value': v}, expected_code=200)
setting.refresh_from_db()
self.assertEqual(setting.to_native_value(), v)
@ -587,14 +566,7 @@ class UserSettingsApiTest(InvenTreeAPITestCase):
# Set invalid options via the API
# Note that this particular setting has a MinValueValidator(1) associated with it
for v in [0, -1, -5]:
response = self.patch(
url,
{
'value': v,
},
expected_code=400,
)
response = self.patch(url, {'value': v}, expected_code=400)
class NotificationUserSettingsApiTest(InvenTreeAPITestCase):
@ -608,9 +580,15 @@ class NotificationUserSettingsApiTest(InvenTreeAPITestCase):
def test_setting(self):
"""Test the string name for NotificationUserSetting."""
NotificationUserSetting.set_setting('NOTIFICATION_METHOD_MAIL', True, change_user=self.user, user=self.user)
test_setting = NotificationUserSetting.get_setting_object('NOTIFICATION_METHOD_MAIL', user=self.user)
self.assertEqual(str(test_setting), 'NOTIFICATION_METHOD_MAIL (for testuser): True')
NotificationUserSetting.set_setting(
'NOTIFICATION_METHOD_MAIL', True, change_user=self.user, user=self.user
)
test_setting = NotificationUserSetting.get_setting_object(
'NOTIFICATION_METHOD_MAIL', user=self.user
)
self.assertEqual(
str(test_setting), 'NOTIFICATION_METHOD_MAIL (for testuser): True'
)
class PluginSettingsApiTest(PluginMixin, InvenTreeAPITestCase):
@ -638,26 +616,38 @@ class PluginSettingsApiTest(PluginMixin, InvenTreeAPITestCase):
registry.set_plugin_state('sample', True)
# get data
url = reverse('api-plugin-setting-detail', kwargs={'plugin': 'sample', 'key': 'API_KEY'})
url = reverse(
'api-plugin-setting-detail', kwargs={'plugin': 'sample', 'key': 'API_KEY'}
)
response = self.get(url, expected_code=200)
# check the right setting came through
self.assertTrue(response.data['key'], 'API_KEY')
self.assertTrue(response.data['plugin'], 'sample')
self.assertTrue(response.data['type'], 'string')
self.assertTrue(response.data['description'], 'Key required for accessing external API')
self.assertTrue(
response.data['description'], 'Key required for accessing external API'
)
# Failure mode tests
# Non-existent plugin
url = reverse('api-plugin-setting-detail', kwargs={'plugin': 'doesnotexist', 'key': 'doesnotmatter'})
url = reverse(
'api-plugin-setting-detail',
kwargs={'plugin': 'doesnotexist', 'key': 'doesnotmatter'},
)
response = self.get(url, expected_code=404)
self.assertIn("Plugin 'doesnotexist' not installed", str(response.data))
# Wrong key
url = reverse('api-plugin-setting-detail', kwargs={'plugin': 'sample', 'key': 'doesnotexist'})
url = reverse(
'api-plugin-setting-detail',
kwargs={'plugin': 'sample', 'key': 'doesnotexist'},
)
response = self.get(url, expected_code=404)
self.assertIn("Plugin 'sample' has no setting matching 'doesnotexist'", str(response.data))
self.assertIn(
"Plugin 'sample' has no setting matching 'doesnotexist'", str(response.data)
)
def test_invalid_setting_key(self):
"""Test that an invalid setting key returns a 404."""
@ -684,32 +674,30 @@ class WebhookMessageTests(TestCase):
def test_missing_token(self):
"""Tests that token checks work."""
response = self.client.post(
self.url,
content_type=CONTENT_TYPE_JSON,
)
response = self.client.post(self.url, content_type=CONTENT_TYPE_JSON)
assert response.status_code == HTTPStatus.FORBIDDEN
assert (
json.loads(response.content)['detail'] == WebhookView.model_class.MESSAGE_TOKEN_ERROR
json.loads(response.content)['detail']
== WebhookView.model_class.MESSAGE_TOKEN_ERROR
)
def test_bad_token(self):
"""Test that a wrong token is not working."""
response = self.client.post(
self.url,
content_type=CONTENT_TYPE_JSON,
**{'HTTP_TOKEN': '1234567fghj'},
self.url, content_type=CONTENT_TYPE_JSON, **{'HTTP_TOKEN': '1234567fghj'}
)
assert response.status_code == HTTPStatus.FORBIDDEN
assert (json.loads(response.content)['detail'] == WebhookView.model_class.MESSAGE_TOKEN_ERROR)
assert (
json.loads(response.content)['detail']
== WebhookView.model_class.MESSAGE_TOKEN_ERROR
)
def test_bad_url(self):
"""Test that a wrongly formed url is not working."""
response = self.client.post(
'/api/webhook/1234/',
content_type=CONTENT_TYPE_JSON,
'/api/webhook/1234/', content_type=CONTENT_TYPE_JSON
)
assert response.status_code == HTTPStatus.NOT_FOUND
@ -725,7 +713,8 @@ class WebhookMessageTests(TestCase):
assert response.status_code == HTTPStatus.NOT_ACCEPTABLE
assert (
json.loads(response.content)['detail'] == 'Expecting property name enclosed in double quotes'
json.loads(response.content)['detail']
== 'Expecting property name enclosed in double quotes'
)
def test_success_no_token_check(self):
@ -735,10 +724,7 @@ class WebhookMessageTests(TestCase):
self.endpoint_def.save()
# check
response = self.client.post(
self.url,
content_type=CONTENT_TYPE_JSON,
)
response = self.client.post(self.url, content_type=CONTENT_TYPE_JSON)
assert response.status_code == HTTPStatus.OK
assert str(response.content, 'utf-8') == WebhookView.model_class.MESSAGE_OK
@ -751,13 +737,13 @@ class WebhookMessageTests(TestCase):
self.endpoint_def.save()
# check
response = self.client.post(
self.url,
content_type=CONTENT_TYPE_JSON,
)
response = self.client.post(self.url, content_type=CONTENT_TYPE_JSON)
assert response.status_code == HTTPStatus.FORBIDDEN
assert (json.loads(response.content)['detail'] == WebhookView.model_class.MESSAGE_TOKEN_ERROR)
assert (
json.loads(response.content)['detail']
== WebhookView.model_class.MESSAGE_TOKEN_ERROR
)
def test_success_hmac(self):
"""Test with a valid HMAC provided."""
@ -783,7 +769,7 @@ class WebhookMessageTests(TestCase):
"""
response = self.client.post(
self.url,
data={"this": "is a message"},
data={'this': 'is a message'},
content_type=CONTENT_TYPE_JSON,
**{'HTTP_TOKEN': str(self.endpoint_def.token)},
)
@ -791,15 +777,13 @@ class WebhookMessageTests(TestCase):
assert response.status_code == HTTPStatus.OK
assert str(response.content, 'utf-8') == WebhookView.model_class.MESSAGE_OK
message = WebhookMessage.objects.get()
assert message.body == {"this": "is a message"}
assert message.body == {'this': 'is a message'}
class NotificationTest(InvenTreeAPITestCase):
"""Tests for NotificationEntry."""
fixtures = [
'users',
]
fixtures = ['users']
def test_check_notification_entries(self):
"""Test that notification entries can be created."""
@ -832,7 +816,10 @@ class NotificationTest(InvenTreeAPITestCase):
self.assertIn('GET', response.data['actions'])
self.assertNotIn('POST', response.data['actions'])
self.assertEqual(response.data['description'], 'List view for all notifications of the current user.')
self.assertEqual(
response.data['description'],
'List view for all notifications of the current user.',
)
# POST action should fail (not allowed)
response = self.post(url, {}, expected_code=405)
@ -867,13 +854,7 @@ class NotificationTest(InvenTreeAPITestCase):
ntf.save()
# Read out via API again
response = self.get(
url,
{
'read': True,
},
expected_code=200
)
response = self.get(url, {'read': True}, expected_code=200)
# Check validity of returned data
self.assertEqual(len(response.data), 3)
@ -882,15 +863,7 @@ class NotificationTest(InvenTreeAPITestCase):
# Now, let's bulk delete all 'unread' notifications via the API,
# but only associated with the logged in user
response = self.delete(
url,
{
'filters': {
'read': False,
}
},
expected_code=204,
)
response = self.delete(url, {'filters': {'read': False}}, expected_code=204)
# Only 7 notifications should have been deleted,
# as the notifications associated with other users must remain untouched
@ -907,13 +880,17 @@ class CommonTest(InvenTreeAPITestCase):
from plugin import registry
# set flag true
common.models.InvenTreeSetting.set_setting('SERVER_RESTART_REQUIRED', True, None)
common.models.InvenTreeSetting.set_setting(
'SERVER_RESTART_REQUIRED', True, None
)
# reload the app
registry.reload_plugins()
# now it should be false again
self.assertFalse(common.models.InvenTreeSetting.get_setting('SERVER_RESTART_REQUIRED'))
self.assertFalse(
common.models.InvenTreeSetting.get_setting('SERVER_RESTART_REQUIRED')
)
def test_config_api(self):
"""Test config URLs."""
@ -926,8 +903,13 @@ class CommonTest(InvenTreeAPITestCase):
# Successful checks
data = [
self.get(reverse('api-config-list'), expected_code=200).data[0], # list endpoint
self.get(reverse('api-config-detail', kwargs={'key': 'INVENTREE_DEBUG'}), expected_code=200).data, # detail endpoint
self.get(reverse('api-config-list'), expected_code=200).data[
0
], # list endpoint
self.get(
reverse('api-config-detail', kwargs={'key': 'INVENTREE_DEBUG'}),
expected_code=200,
).data, # detail endpoint
]
for item in data:
@ -956,21 +938,33 @@ class CommonTest(InvenTreeAPITestCase):
self.assertEqual(response.data[0]['key'], 'EXPERIMENTAL')
self.assertTrue(response.data[0]['conditions'])
response = self.get(reverse('api-flag-detail', kwargs={'key': 'EXPERIMENTAL'}), expected_code=200)
response = self.get(
reverse('api-flag-detail', kwargs={'key': 'EXPERIMENTAL'}),
expected_code=200,
)
self.assertEqual(len(response.data), 3)
self.assertEqual(response.data['key'], 'EXPERIMENTAL')
self.assertTrue(response.data['conditions'])
# Try without param -> false
response = self.get(reverse('api-flag-detail', kwargs={'key': 'NEXT_GEN'}), expected_code=200)
response = self.get(
reverse('api-flag-detail', kwargs={'key': 'NEXT_GEN'}), expected_code=200
)
self.assertFalse(response.data['state'])
# Try with param -> true
response = self.get(reverse('api-flag-detail', kwargs={'key': 'NEXT_GEN'}), {'ngen': ''}, expected_code=200)
response = self.get(
reverse('api-flag-detail', kwargs={'key': 'NEXT_GEN'}),
{'ngen': ''},
expected_code=200,
)
self.assertTrue(response.data['state'])
# Try non existent flag
response = self.get(reverse('api-flag-detail', kwargs={'key': 'NON_EXISTENT'}), expected_code=404)
response = self.get(
reverse('api-flag-detail', kwargs={'key': 'NON_EXISTENT'}),
expected_code=404,
)
# Turn into normal user again
self.user.is_superuser = False
@ -1038,7 +1032,7 @@ class CurrencyAPITests(InvenTreeAPITestCase):
# Delay and try again
time.sleep(10)
raise TimeoutError("Could not refresh currency exchange data after 5 attempts")
raise TimeoutError('Could not refresh currency exchange data after 5 attempts')
class NotesImageTest(InvenTreeAPITestCase):
@ -1052,25 +1046,29 @@ class NotesImageTest(InvenTreeAPITestCase):
response = self.post(
reverse('api-notes-image-list'),
data={
'image': SimpleUploadedFile('test.txt', b"this is not an image file", content_type='text/plain'),
},
format='multipart',
expected_code=400
)
self.assertIn("Upload a valid image", str(response.data['image']))
# Test upload of an invalid image file
response = self.post(
reverse('api-notes-image-list'),
data={
'image': SimpleUploadedFile('test.png', b"this is not an image file", content_type='image/png'),
'image': SimpleUploadedFile(
'test.txt', b'this is not an image file', content_type='text/plain'
)
},
format='multipart',
expected_code=400,
)
self.assertIn("Upload a valid image", str(response.data['image']))
self.assertIn('Upload a valid image', str(response.data['image']))
# Test upload of an invalid image file
response = self.post(
reverse('api-notes-image-list'),
data={
'image': SimpleUploadedFile(
'test.png', b'this is not an image file', content_type='image/png'
)
},
format='multipart',
expected_code=400,
)
self.assertIn('Upload a valid image', str(response.data['image']))
# Check that no extra database entries have been created
self.assertEqual(NotesImage.objects.count(), n)
@ -1089,10 +1087,12 @@ class NotesImageTest(InvenTreeAPITestCase):
self.post(
reverse('api-notes-image-list'),
data={
'image': SimpleUploadedFile('test.png', contents, content_type='image/png'),
'image': SimpleUploadedFile(
'test.png', contents, content_type='image/png'
)
},
format='multipart',
expected_code=201
expected_code=201,
)
# Check that a new file has been created
@ -1136,7 +1136,7 @@ class ProjectCodesTest(InvenTreeAPITestCase):
# Delete it
self.delete(
reverse('api-project-code-detail', kwargs={'pk': code.pk}),
expected_code=204
expected_code=204,
)
# Check it is gone
@ -1147,25 +1147,22 @@ class ProjectCodesTest(InvenTreeAPITestCase):
# Create a new project code
response = self.post(
self.url,
data={
'code': 'PRJ-001',
'description': 'Test project code',
},
expected_code=400
data={'code': 'PRJ-001', 'description': 'Test project code'},
expected_code=400,
)
self.assertIn('project code with this Project Code already exists', str(response.data['code']))
self.assertIn(
'project code with this Project Code already exists',
str(response.data['code']),
)
def test_write_access(self):
"""Test that non-staff users have read-only access"""
# By default user has staff access, can create a new project code
response = self.post(
self.url,
data={
'code': 'PRJ-xxx',
'description': 'Test project code',
},
expected_code=201
data={'code': 'PRJ-xxx', 'description': 'Test project code'},
expected_code=201,
)
pk = response.data['pk']
@ -1173,10 +1170,8 @@ class ProjectCodesTest(InvenTreeAPITestCase):
# Test we can edit, also
response = self.patch(
reverse('api-project-code-detail', kwargs={'pk': pk}),
data={
'code': 'PRJ-999',
},
expected_code=200
data={'code': 'PRJ-999'},
expected_code=200,
)
self.assertEqual(response.data['code'], 'PRJ-999')
@ -1188,20 +1183,15 @@ class ProjectCodesTest(InvenTreeAPITestCase):
# As user does not have staff access, should return 403 for list endpoint
response = self.post(
self.url,
data={
'code': 'PRJ-123',
'description': 'Test project code'
},
expected_code=403
data={'code': 'PRJ-123', 'description': 'Test project code'},
expected_code=403,
)
# Should also return 403 for detail endpoint
response = self.patch(
reverse('api-project-code-detail', kwargs={'pk': pk}),
data={
'code': 'PRJ-999',
},
expected_code=403
data={'code': 'PRJ-999'},
expected_code=403,
)
@ -1219,8 +1209,14 @@ class CustomUnitAPITest(InvenTreeAPITestCase):
super().setUpTestData()
units = [
CustomUnit(name='metres_per_amp', definition='meter / ampere', symbol='m/A'),
CustomUnit(name='hectares_per_second', definition='hectares per second', symbol='ha/s'),
CustomUnit(
name='metres_per_amp', definition='meter / ampere', symbol='m/A'
),
CustomUnit(
name='hectares_per_second',
definition='hectares per second',
symbol='ha/s',
),
]
CustomUnit.objects.bulk_create(units)
@ -1240,10 +1236,8 @@ class CustomUnitAPITest(InvenTreeAPITestCase):
self.patch(
reverse('api-custom-unit-detail', kwargs={'pk': unit.pk}),
{
'name': 'new_unit_name',
},
expected_code=403
{'name': 'new_unit_name'},
expected_code=403,
)
# Ok, what if we have permission?
@ -1252,9 +1246,7 @@ class CustomUnitAPITest(InvenTreeAPITestCase):
self.patch(
reverse('api-custom-unit-detail', kwargs={'pk': unit.pk}),
{
'name': 'new_unit_name',
},
{'name': 'new_unit_name'},
# expected_code=200
)
@ -1269,21 +1261,9 @@ class CustomUnitAPITest(InvenTreeAPITestCase):
self.user.save()
# Test invalid 'name' values (must be valid identifier)
invalid_name_values = [
'1',
'1abc',
'abc def',
'abc-def',
'abc.def',
]
invalid_name_values = ['1', '1abc', 'abc def', 'abc-def', 'abc.def']
url = reverse('api-custom-unit-detail', kwargs={'pk': unit.pk})
for name in invalid_name_values:
self.patch(
url,
{
'name': name,
},
expected_code=400
)
self.patch(url, {'name': name}, expected_code=400)

View File

@ -1,4 +1,3 @@
"""URL lookup for common views."""
common_urls = [
]
common_urls = []

View File

@ -81,11 +81,7 @@ class FileManagementFormView(MultiStepFormView):
('fields', forms.MatchFieldForm),
('items', forms.MatchItemForm),
]
form_steps_description = [
_("Upload File"),
_("Match Fields"),
_("Match Items"),
]
form_steps_description = [_('Upload File'), _('Match Fields'), _('Match Items')]
media_folder = 'file_upload/'
extra_context_data = {}
@ -95,8 +91,12 @@ class FileManagementFormView(MultiStepFormView):
super().__init__(self, *args, **kwargs)
# Check for file manager class
if not hasattr(self, 'file_manager_class') and not issubclass(self.file_manager_class, FileManager):
raise NotImplementedError('A subclass of a file manager class needs to be set!')
if not hasattr(self, 'file_manager_class') and not issubclass(
self.file_manager_class, FileManager
):
raise NotImplementedError(
'A subclass of a file manager class needs to be set!'
)
def get_context_data(self, form=None, **kwargs):
"""Handle context data."""
@ -106,7 +106,6 @@ class FileManagementFormView(MultiStepFormView):
context = super().get_context_data(form=form, **kwargs)
if self.steps.current in ('fields', 'items'):
# Get columns and row data
self.columns = self.file_manager.columns()
self.rows = self.file_manager.rows()
@ -140,7 +139,9 @@ class FileManagementFormView(MultiStepFormView):
# Get file
file = upload_files.get('upload-file', None)
if file:
self.file_manager = self.file_manager_class(file=file, name=self.name)
self.file_manager = self.file_manager_class(
file=file, name=self.name
)
def get_form_kwargs(self, step=None):
"""Update kwargs to dynamically build forms."""
@ -150,15 +151,11 @@ class FileManagementFormView(MultiStepFormView):
if step == 'upload':
# Dynamically build upload form
if self.name:
kwargs = {
'name': self.name
}
kwargs = {'name': self.name}
return kwargs
elif step == 'fields':
# Dynamically build match field form
kwargs = {
'file_manager': self.file_manager
}
kwargs = {'file_manager': self.file_manager}
return kwargs
elif step == 'items':
# Dynamically build match item form
@ -206,7 +203,6 @@ class FileManagementFormView(MultiStepFormView):
self.row_data = {}
for item, value in form_data.items():
# Column names as passed as col_name_<idx> where idx is an integer
# Extract the column names
@ -220,7 +216,6 @@ class FileManagementFormView(MultiStepFormView):
# Extract the column selections (in the 'select fields' view)
if item.startswith('fields-'):
try:
col_name = item.replace('fields-', '')
except ValueError:
@ -258,10 +253,7 @@ class FileManagementFormView(MultiStepFormView):
self.columns = []
for idx, value in self.column_names.items():
header = ({
'name': value,
'guess': self.column_selections.get(idx, ''),
})
header = {'name': value, 'guess': self.column_selections.get(idx, '')}
self.columns.append(header)
if self.row_data:
@ -280,18 +272,10 @@ class FileManagementFormView(MultiStepFormView):
'guess': self.column_selections[idx],
}
cell_data = {
'cell': item,
'idx': idx,
'column': column_data,
}
cell_data = {'cell': item, 'idx': idx, 'column': column_data}
data.append(cell_data)
row = {
'index': row_idx,
'data': data,
'errors': {},
}
row = {'index': row_idx, 'data': data, 'errors': {}}
self.rows.append(row)
@ -344,11 +328,7 @@ class FileManagementFormView(MultiStepFormView):
try:
if idx not in items:
# Insert into items
items.update({
idx: {
self.form_field_map[field]: form_value,
}
})
items.update({idx: {self.form_field_map[field]: form_value}})
else:
# Update items
items[idx][self.form_field_map[field]] = form_value
@ -383,14 +363,15 @@ class FileManagementFormView(MultiStepFormView):
duplicates = []
for col in self.column_names:
if col in self.column_selections:
guess = self.column_selections[col]
else:
guess = None
if guess:
n = list(self.column_selections.values()).count(self.column_selections[col])
n = list(self.column_selections.values()).count(
self.column_selections[col]
)
if n > 1 and self.column_selections[col] not in duplicates:
duplicates.append(self.column_selections[col])
@ -459,7 +440,9 @@ class FileManagementAjaxView(AjaxView):
wizard_back = self.request.POST.get('act-btn_back', None)
if wizard_back:
back_step_index = self.get_step_index() - 1
self.storage.current_step = list(self.get_form_list().keys())[back_step_index]
self.storage.current_step = list(self.get_form_list().keys())[
back_step_index
]
return self.renderJsonResponse(request, data={'form_valid': None})
# validate form
@ -499,13 +482,19 @@ class FileManagementAjaxView(AjaxView):
data = {}
self.setTemplate()
return super().renderJsonResponse(request, form=form, data=data, context=context)
return super().renderJsonResponse(
request, form=form, data=data, context=context
)
def get_data(self) -> dict:
"""Get extra context data."""
data = super().get_data()
data['hideErrorMessage'] = '1' # hide the error
buttons = [{'name': 'back', 'title': _('Previous Step')}] if self.get_step_index() > 0 else []
buttons = (
[{'name': 'back', 'title': _('Previous Step')}]
if self.get_step_index() > 0
else []
)
data['buttons'] = buttons # set buttons
return data