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: commitf5cf7b2e78
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:36:57 2024 +0100 fixed reqs commit9d845bee98
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:32:35 2024 +0100 disable autofix/format commitaff5f27148
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:28:50 2024 +0100 adjust checks commit47271cf1ef
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:28:22 2024 +0100 reorder order of operations commite1bf178b40
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:01:09 2024 +0100 adapted ruff settings to better fit code base commitad7d88a6f4
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 19:59:45 2024 +0100 auto fixed docstring commita2e54a760e
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 19:46:35 2024 +0100 fix getattr useage commitcb80c73bc6
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 19:25:09 2024 +0100 fix requirements file commitb7780bbd21
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:42:28 2024 +0100 fix removed sections commit71f1681f55
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:41:21 2024 +0100 fix djlint syntax commita0bcf1bcce
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:35:28 2024 +0100 remove flake8 from code base commit22475b31cc
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:34:56 2024 +0100 remove flake8 from code base commit0413350f14
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:24:39 2024 +0100 moved ruff section commitd90c48a0bf
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:24:24 2024 +0100 move djlint config to pyproject commitc5ce55d511
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:20:39 2024 +0100 added isort again commit42a41d23af
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:19:02 2024 +0100 move config section commit8569233181
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:17:52 2024 +0100 fix codespell error commit2897c6704d
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: commitd3b795824b
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 22:56:17 2024 +0100 fixed source path commit0bac0c19b8
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 22:47:53 2024 +0100 fixed req commit9f61f01d9c
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 22:45:18 2024 +0100 added missing toml req commit91b71ed24a
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:49:50 2024 +0100 moved isort config commit12460b0419
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:43:22 2024 +0100 remove flake8 section from setup.cfg commitf5cf7b2e78
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:36:57 2024 +0100 fixed reqs commit9d845bee98
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:32:35 2024 +0100 disable autofix/format commitaff5f27148
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:28:50 2024 +0100 adjust checks commit47271cf1ef
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:28:22 2024 +0100 reorder order of operations commite1bf178b40
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:01:09 2024 +0100 adapted ruff settings to better fit code base commitad7d88a6f4
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 19:59:45 2024 +0100 auto fixed docstring commita2e54a760e
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 19:46:35 2024 +0100 fix getattr useage commitcb80c73bc6
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 19:25:09 2024 +0100 fix requirements file commitb7780bbd21
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:42:28 2024 +0100 fix removed sections commit71f1681f55
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:41:21 2024 +0100 fix djlint syntax commita0bcf1bcce
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:35:28 2024 +0100 remove flake8 from code base commit22475b31cc
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:34:56 2024 +0100 remove flake8 from code base commit0413350f14
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:24:39 2024 +0100 moved ruff section commitd90c48a0bf
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:24:24 2024 +0100 move djlint config to pyproject commitc5ce55d511
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:20:39 2024 +0100 added isort again commit42a41d23af
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:19:02 2024 +0100 move config section commit8569233181
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:17:52 2024 +0100 fix codespell error commit2897c6704d
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:
@ -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)
|
||||
|
@ -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 = [
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
@ -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')
|
||||
|
@ -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']
|
||||
|
@ -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 = ''
|
||||
|
@ -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))
|
||||
|
@ -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')
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -1,4 +1,3 @@
|
||||
"""URL lookup for common views."""
|
||||
|
||||
common_urls = [
|
||||
]
|
||||
common_urls = []
|
||||
|
@ -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
|
||||
|
||||
|
Reference in New Issue
Block a user