mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-18 04:55:44 +00:00
Docstring checks in QC checks (#3089)
* Add pre-commit to the stack * exclude static * Add locales to excludes * fix style errors * rename pipeline steps * also wait on precommit * make template matching simpler * Use the same code for python setup everywhere * use step and cache for python setup * move regular settings up into general envs * just use full update * Use invoke instead of static references * make setup actions more similar * use python3 * refactor names to be similar * fix runner version * fix references * remove incidential change * use matrix for os * Github can't do this right now * ignore docstyle errors * Add seperate docstring test * update flake call * do not fail on docstring * refactor setup into workflow * update reference * switch to action * resturcture * add bash statements * remove os from cache * update input checks * make code cleaner * fix boolean * no relative paths * install wheel by python * switch to install * revert back to simple wheel * refactor import export tests * move setup keys back to not disturbe tests * remove docstyle till that is fixed * update references * continue on error * add docstring test * use relativ action references * Change step / job docstrings * update to merge * reformat comments 1 * fix docstrings 2 * fix docstrings 3 * fix docstrings 4 * fix docstrings 5 * fix docstrings 6 * fix docstrings 7 * fix docstrings 8 * fix docstirns 9 * fix docstrings 10 * docstring adjustments * update the remaining docstrings * small docstring changes * fix function name * update support files for docstrings * Add missing args to docstrings * Remove outdated function * Add docstrings for the 'build' app * Make API code cleaner * add more docstrings for plugin app * Remove dead code for plugin settings No idea what that was even intended for * ignore __init__ files for docstrings * More docstrings * Update docstrings for the 'part' directory * Fixes for related_part functionality * Fix removed stuff from merge99676ee
* make more consistent * Show statistics for docstrings * add more docstrings * move specific register statements to make them clearer to understant * More docstrings for common * and more docstrings * and more * simpler call * docstrings for notifications * docstrings for common/tests * Add docs for common/models * Revert "move specific register statements to make them clearer to understant" This reverts commitca96654622
. * use typing here * Revert "Make API code cleaner" This reverts commit24fb68bd3e
. * docstring updates for the 'users' app * Add generic Meta info to simple Meta classes * remove unneeded unique_together statements * More simple metas * Remove unnecessary format specifier * Remove extra json format specifiers * Add docstrings for the 'plugin' app * Docstrings for the 'label' app * Add missing docstrings for the 'report' app * Fix build test regression * Fix top-level files * docstrings for InvenTree/InvenTree * reduce unneeded code * add docstrings * and more docstrings * more docstrings * more docstrings for stock * more docstrings * docstrings for order/views * Docstrings for various files in the 'order' app * Docstrings for order/test_api.py * Docstrings for order/serializers.py * Docstrings for order/admin.py * More docstrings for the order app * Add docstrings for the 'company' app * Add unit tests for rebuilding the reference fields * Prune out some more dead code * remove more dead code Co-authored-by: Oliver Walters <oliver.henry.walters@gmail.com>
This commit is contained in:
@ -1,3 +1,5 @@
|
||||
"""Admin for the common app."""
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from import_export.admin import ImportExportModelAdmin
|
||||
@ -6,14 +8,12 @@ import common.models
|
||||
|
||||
|
||||
class SettingsAdmin(ImportExportModelAdmin):
|
||||
"""Admin settings for InvenTreeSetting."""
|
||||
|
||||
list_display = ('key', 'value')
|
||||
|
||||
def get_readonly_fields(self, request, obj=None): # pragma: no cover
|
||||
"""
|
||||
Prevent the 'key' field being edited once the setting is created
|
||||
"""
|
||||
|
||||
"""Prevent the 'key' field being edited once the setting is created."""
|
||||
if obj:
|
||||
return ['key']
|
||||
else:
|
||||
@ -21,14 +21,12 @@ class SettingsAdmin(ImportExportModelAdmin):
|
||||
|
||||
|
||||
class UserSettingsAdmin(ImportExportModelAdmin):
|
||||
"""Admin settings for InvenTreeUserSetting."""
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
"""Prevent the 'key' field being edited once the setting is created."""
|
||||
if obj:
|
||||
return ['key']
|
||||
else:
|
||||
@ -36,16 +34,19 @@ class UserSettingsAdmin(ImportExportModelAdmin):
|
||||
|
||||
|
||||
class WebhookAdmin(ImportExportModelAdmin):
|
||||
"""Admin settings for Webhook."""
|
||||
|
||||
list_display = ('endpoint_id', 'name', 'active', 'user')
|
||||
|
||||
|
||||
class NotificationEntryAdmin(admin.ModelAdmin):
|
||||
"""Admin settings for NotificationEntry."""
|
||||
|
||||
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', )
|
||||
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
Provides a JSON API for common components.
|
||||
"""
|
||||
"""Provides a JSON API for common components."""
|
||||
|
||||
import json
|
||||
|
||||
@ -24,25 +22,23 @@ from plugin.serializers import NotificationUserSettingSerializer
|
||||
|
||||
|
||||
class CsrfExemptMixin(object):
|
||||
"""
|
||||
Exempts the view from CSRF requirements.
|
||||
"""
|
||||
"""Exempts the view from CSRF requirements."""
|
||||
|
||||
@method_decorator(csrf_exempt)
|
||||
def dispatch(self, *args, **kwargs):
|
||||
return super(CsrfExemptMixin, self).dispatch(*args, **kwargs)
|
||||
"""Overwrites dispatch to be extempt from csrf checks."""
|
||||
return super().dispatch(*args, **kwargs)
|
||||
|
||||
|
||||
class WebhookView(CsrfExemptMixin, APIView):
|
||||
"""
|
||||
Endpoint for receiving webhooks.
|
||||
"""
|
||||
"""Endpoint for receiving webhooks."""
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
model_class = common.models.WebhookEndpoint
|
||||
run_async = False
|
||||
|
||||
def post(self, request, endpoint, *args, **kwargs):
|
||||
"""Process incomming webhook."""
|
||||
# get webhook definition
|
||||
self._get_webhook(endpoint, request, *args, **kwargs)
|
||||
|
||||
@ -101,6 +97,10 @@ class WebhookView(CsrfExemptMixin, APIView):
|
||||
|
||||
|
||||
class SettingsList(generics.ListAPIView):
|
||||
"""Generic ListView for settings.
|
||||
|
||||
This is inheritted by all list views for settings.
|
||||
"""
|
||||
|
||||
filter_backends = [
|
||||
DjangoFilterBackend,
|
||||
@ -120,24 +120,17 @@ class SettingsList(generics.ListAPIView):
|
||||
|
||||
|
||||
class GlobalSettingsList(SettingsList):
|
||||
"""
|
||||
API endpoint for accessing a list of global settings objects
|
||||
"""
|
||||
"""API endpoint for accessing a list of global settings objects."""
|
||||
|
||||
queryset = common.models.InvenTreeSetting.objects.all()
|
||||
serializer_class = common.serializers.GlobalSettingsSerializer
|
||||
|
||||
|
||||
class GlobalSettingsPermissions(permissions.BasePermission):
|
||||
"""
|
||||
Special permission class to determine if the user is "staff"
|
||||
"""
|
||||
"""Special permission class to determine if the user is "staff"."""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
"""
|
||||
Check that the requesting user is 'admin'
|
||||
"""
|
||||
|
||||
"""Check that the requesting user is 'admin'."""
|
||||
try:
|
||||
user = request.user
|
||||
|
||||
@ -152,8 +145,7 @@ class GlobalSettingsPermissions(permissions.BasePermission):
|
||||
|
||||
|
||||
class GlobalSettingsDetail(generics.RetrieveUpdateAPIView):
|
||||
"""
|
||||
Detail view for an individual "global setting" object.
|
||||
"""Detail view for an individual "global setting" object.
|
||||
|
||||
- User must have 'staff' status to view / edit
|
||||
"""
|
||||
@ -163,10 +155,7 @@ class GlobalSettingsDetail(generics.RetrieveUpdateAPIView):
|
||||
serializer_class = common.serializers.GlobalSettingsSerializer
|
||||
|
||||
def get_object(self):
|
||||
"""
|
||||
Attempt to find a global setting object with the provided key.
|
||||
"""
|
||||
|
||||
"""Attempt to find a global setting object with the provided key."""
|
||||
key = self.kwargs['key']
|
||||
|
||||
if key not in common.models.InvenTreeSetting.SETTINGS.keys():
|
||||
@ -181,18 +170,13 @@ class GlobalSettingsDetail(generics.RetrieveUpdateAPIView):
|
||||
|
||||
|
||||
class UserSettingsList(SettingsList):
|
||||
"""
|
||||
API endpoint for accessing a list of user settings objects
|
||||
"""
|
||||
"""API endpoint for accessing a list of user settings objects."""
|
||||
|
||||
queryset = common.models.InvenTreeUserSetting.objects.all()
|
||||
serializer_class = common.serializers.UserSettingsSerializer
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
"""
|
||||
Only list settings which apply to the current user
|
||||
"""
|
||||
|
||||
"""Only list settings which apply to the current user."""
|
||||
try:
|
||||
user = self.request.user
|
||||
except AttributeError: # pragma: no cover
|
||||
@ -206,12 +190,10 @@ class UserSettingsList(SettingsList):
|
||||
|
||||
|
||||
class UserSettingsPermissions(permissions.BasePermission):
|
||||
"""
|
||||
Special permission class to determine if the user can view / edit a particular setting
|
||||
"""
|
||||
"""Special permission class to determine if the user can view / edit a particular setting."""
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
|
||||
"""Check if the user that requested is also the object owner."""
|
||||
try:
|
||||
user = request.user
|
||||
except AttributeError: # pragma: no cover
|
||||
@ -221,8 +203,7 @@ class UserSettingsPermissions(permissions.BasePermission):
|
||||
|
||||
|
||||
class UserSettingsDetail(generics.RetrieveUpdateAPIView):
|
||||
"""
|
||||
Detail view for an individual "user setting" object
|
||||
"""Detail view for an individual "user setting" object.
|
||||
|
||||
- User can only view / edit settings their own settings objects
|
||||
"""
|
||||
@ -232,10 +213,7 @@ class UserSettingsDetail(generics.RetrieveUpdateAPIView):
|
||||
serializer_class = common.serializers.UserSettingsSerializer
|
||||
|
||||
def get_object(self):
|
||||
"""
|
||||
Attempt to find a user setting object with the provided key.
|
||||
"""
|
||||
|
||||
"""Attempt to find a user setting object with the provided key."""
|
||||
key = self.kwargs['key']
|
||||
|
||||
if key not in common.models.InvenTreeUserSetting.SETTINGS.keys():
|
||||
@ -249,18 +227,13 @@ class UserSettingsDetail(generics.RetrieveUpdateAPIView):
|
||||
|
||||
|
||||
class NotificationUserSettingsList(SettingsList):
|
||||
"""
|
||||
API endpoint for accessing a list of notification user settings objects
|
||||
"""
|
||||
"""API endpoint for accessing a list of notification user settings objects."""
|
||||
|
||||
queryset = NotificationUserSetting.objects.all()
|
||||
serializer_class = NotificationUserSettingSerializer
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
"""
|
||||
Only list settings which apply to the current user
|
||||
"""
|
||||
|
||||
"""Only list settings which apply to the current user."""
|
||||
try:
|
||||
user = self.request.user
|
||||
except AttributeError:
|
||||
@ -272,8 +245,7 @@ class NotificationUserSettingsList(SettingsList):
|
||||
|
||||
|
||||
class NotificationUserSettingsDetail(generics.RetrieveUpdateAPIView):
|
||||
"""
|
||||
Detail view for an individual "notification user setting" object
|
||||
"""Detail view for an individual "notification user setting" object.
|
||||
|
||||
- User can only view / edit settings their own settings objects
|
||||
"""
|
||||
@ -287,6 +259,8 @@ class NotificationUserSettingsDetail(generics.RetrieveUpdateAPIView):
|
||||
|
||||
|
||||
class NotificationList(generics.ListAPIView):
|
||||
"""List view for all notifications of the current user."""
|
||||
|
||||
queryset = common.models.NotificationMessage.objects.all()
|
||||
serializer_class = common.serializers.NotificationMessageSerializer
|
||||
|
||||
@ -313,10 +287,7 @@ class NotificationList(generics.ListAPIView):
|
||||
]
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
"""
|
||||
Only list notifications which apply to the current user
|
||||
"""
|
||||
|
||||
"""Only list notifications which apply to the current user."""
|
||||
try:
|
||||
user = self.request.user
|
||||
except AttributeError:
|
||||
@ -328,8 +299,7 @@ class NotificationList(generics.ListAPIView):
|
||||
|
||||
|
||||
class NotificationDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
Detail view for an individual notification object
|
||||
"""Detail view for an individual notification object.
|
||||
|
||||
- User can only view / delete their own notification objects
|
||||
"""
|
||||
@ -342,9 +312,7 @@ class NotificationDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
|
||||
|
||||
class NotificationReadEdit(generics.CreateAPIView):
|
||||
"""
|
||||
general API endpoint to manipulate read state of a notification
|
||||
"""
|
||||
"""General API endpoint to manipulate read state of a notification."""
|
||||
|
||||
queryset = common.models.NotificationMessage.objects.all()
|
||||
serializer_class = common.serializers.NotificationReadSerializer
|
||||
@ -354,12 +322,14 @@ class NotificationReadEdit(generics.CreateAPIView):
|
||||
]
|
||||
|
||||
def get_serializer_context(self):
|
||||
"""Add instance to context so it can be accessed in the serializer."""
|
||||
context = super().get_serializer_context()
|
||||
if self.request:
|
||||
context['instance'] = self.get_object()
|
||||
return context
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""Set the `read` status to the target value."""
|
||||
message = self.get_object()
|
||||
try:
|
||||
message.read = self.target
|
||||
@ -369,23 +339,17 @@ class NotificationReadEdit(generics.CreateAPIView):
|
||||
|
||||
|
||||
class NotificationRead(NotificationReadEdit):
|
||||
"""
|
||||
API endpoint to mark a notification as read.
|
||||
"""
|
||||
"""API endpoint to mark a notification as read."""
|
||||
target = True
|
||||
|
||||
|
||||
class NotificationUnread(NotificationReadEdit):
|
||||
"""
|
||||
API endpoint to mark a notification as unread.
|
||||
"""
|
||||
"""API endpoint to mark a notification as unread."""
|
||||
target = False
|
||||
|
||||
|
||||
class NotificationReadAll(generics.RetrieveAPIView):
|
||||
"""
|
||||
API endpoint to mark all notifications as read.
|
||||
"""
|
||||
"""API endpoint to mark all notifications as read."""
|
||||
|
||||
queryset = common.models.NotificationMessage.objects.all()
|
||||
|
||||
@ -394,6 +358,7 @@ class NotificationReadAll(generics.RetrieveAPIView):
|
||||
]
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Set all messages for the current user as read."""
|
||||
try:
|
||||
self.queryset.filter(user=request.user, read=False).update(read=True)
|
||||
return Response({'status': 'ok'})
|
||||
|
@ -1,4 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""App config for common app."""
|
||||
|
||||
import logging
|
||||
|
||||
@ -8,17 +8,19 @@ logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
class CommonConfig(AppConfig):
|
||||
"""AppConfig for common app.
|
||||
|
||||
Clears system wide flags on ready.
|
||||
"""
|
||||
|
||||
name = 'common'
|
||||
|
||||
def ready(self):
|
||||
|
||||
"""Initialize restart flag clearance on startup."""
|
||||
self.clear_restart_flag()
|
||||
|
||||
def clear_restart_flag(self):
|
||||
"""
|
||||
Clear the SERVER_RESTART_REQUIRED setting
|
||||
"""
|
||||
|
||||
"""Clear the SERVER_RESTART_REQUIRED setting."""
|
||||
try:
|
||||
import common.models
|
||||
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
Files management tools.
|
||||
"""
|
||||
"""Files management tools."""
|
||||
|
||||
import os
|
||||
|
||||
@ -12,7 +10,7 @@ from rapidfuzz import fuzz
|
||||
|
||||
|
||||
class FileManager:
|
||||
""" Class for managing an uploaded file """
|
||||
"""Class for managing an uploaded file."""
|
||||
|
||||
name = ''
|
||||
|
||||
@ -32,8 +30,7 @@ class FileManager:
|
||||
HEADERS = []
|
||||
|
||||
def __init__(self, file, name=None):
|
||||
""" Initialize the FileManager class with a user-uploaded file object """
|
||||
|
||||
"""Initialize the FileManager class with a user-uploaded file object."""
|
||||
# Set name
|
||||
if name:
|
||||
self.name = name
|
||||
@ -46,8 +43,7 @@ class FileManager:
|
||||
|
||||
@classmethod
|
||||
def validate(cls, file):
|
||||
""" Validate file extension and data """
|
||||
|
||||
"""Validate file extension and data."""
|
||||
cleaned_data = None
|
||||
|
||||
ext = os.path.splitext(file.name)[-1].lower().replace('.', '')
|
||||
@ -79,21 +75,15 @@ class FileManager:
|
||||
return cleaned_data
|
||||
|
||||
def process(self, file):
|
||||
""" Process file """
|
||||
|
||||
"""Process file."""
|
||||
self.data = self.__class__.validate(file)
|
||||
|
||||
def update_headers(self):
|
||||
""" Update headers """
|
||||
|
||||
"""Update headers."""
|
||||
self.HEADERS = self.REQUIRED_HEADERS + self.ITEM_MATCH_HEADERS + self.OPTIONAL_MATCH_HEADERS + self.OPTIONAL_HEADERS
|
||||
|
||||
def setup(self):
|
||||
"""
|
||||
Setup headers
|
||||
should be overriden in usage to set the Different Headers
|
||||
"""
|
||||
|
||||
"""Setup headers should be overriden in usage to set the Different Headers."""
|
||||
if not self.name:
|
||||
return
|
||||
|
||||
@ -101,14 +91,15 @@ class FileManager:
|
||||
self.update_headers()
|
||||
|
||||
def guess_header(self, header, threshold=80):
|
||||
"""
|
||||
Try to match a header (from the file) to a list of known headers
|
||||
"""Try to match a header (from the file) to a list of known headers.
|
||||
|
||||
Args:
|
||||
header - Header name to look for
|
||||
threshold - Match threshold for fuzzy search
|
||||
"""
|
||||
header (Any): Header name to look for
|
||||
threshold (int, optional): Match threshold for fuzzy search. Defaults to 80.
|
||||
|
||||
Returns:
|
||||
Any: Matched headers
|
||||
"""
|
||||
# Replace null values with empty string
|
||||
if header is None:
|
||||
header = ''
|
||||
@ -143,7 +134,7 @@ class FileManager:
|
||||
return None
|
||||
|
||||
def columns(self):
|
||||
""" Return a list of headers for the thingy """
|
||||
"""Return a list of headers for the thingy."""
|
||||
headers = []
|
||||
|
||||
for header in self.data.headers:
|
||||
@ -170,21 +161,21 @@ class FileManager:
|
||||
return headers
|
||||
|
||||
def col_count(self):
|
||||
"""Return the number of columns in the file."""
|
||||
if self.data is None:
|
||||
return 0
|
||||
|
||||
return len(self.data.headers)
|
||||
|
||||
def row_count(self):
|
||||
""" Return the number of rows in the file. """
|
||||
|
||||
"""Return the number of rows in the file."""
|
||||
if self.data is None:
|
||||
return 0
|
||||
|
||||
return len(self.data)
|
||||
|
||||
def rows(self):
|
||||
""" Return a list of all rows """
|
||||
"""Return a list of all rows."""
|
||||
rows = []
|
||||
|
||||
for i in range(self.row_count()):
|
||||
@ -221,15 +212,14 @@ class FileManager:
|
||||
return rows
|
||||
|
||||
def get_row_data(self, index):
|
||||
""" Retrieve row data at a particular index """
|
||||
"""Retrieve row data at a particular index."""
|
||||
if self.data is None or index >= len(self.data):
|
||||
return None
|
||||
|
||||
return self.data[index]
|
||||
|
||||
def get_row_dict(self, index):
|
||||
""" Retrieve a dict object representing the data row at a particular offset """
|
||||
|
||||
"""Retrieve a dict object representing the data row at a particular offset."""
|
||||
if self.data is None or index >= len(self.data):
|
||||
return None
|
||||
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
Django forms for interacting with common objects
|
||||
"""
|
||||
"""Django forms for interacting with common objects."""
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
@ -12,11 +10,11 @@ from .models import InvenTreeSetting
|
||||
|
||||
|
||||
class SettingEditForm(HelperForm):
|
||||
"""
|
||||
Form for creating / editing a settings object
|
||||
"""
|
||||
"""Form for creating / editing a settings object."""
|
||||
|
||||
class Meta:
|
||||
"""Metaclassoptions for SettingEditForm."""
|
||||
|
||||
model = InvenTreeSetting
|
||||
|
||||
fields = [
|
||||
@ -25,7 +23,7 @@ class SettingEditForm(HelperForm):
|
||||
|
||||
|
||||
class UploadFileForm(forms.Form):
|
||||
""" Step 1 of FileManagementFormView """
|
||||
"""Step 1 of FileManagementFormView."""
|
||||
|
||||
file = forms.FileField(
|
||||
label=_('File'),
|
||||
@ -33,8 +31,7 @@ class UploadFileForm(forms.Form):
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
""" Update label and help_text """
|
||||
|
||||
"""Update label and help_text."""
|
||||
# Get file name
|
||||
name = None
|
||||
if 'name' in kwargs:
|
||||
@ -48,11 +45,10 @@ class UploadFileForm(forms.Form):
|
||||
self.fields['file'].help_text = _(f'Select {name} file to upload')
|
||||
|
||||
def clean_file(self):
|
||||
"""
|
||||
Run tabular file validation.
|
||||
If anything is wrong with the file, it will raise ValidationError
|
||||
"""
|
||||
"""Run tabular file validation.
|
||||
|
||||
If anything is wrong with the file, it will raise ValidationError
|
||||
"""
|
||||
file = self.cleaned_data['file']
|
||||
|
||||
# Validate file using FileManager class - will perform initial data validation
|
||||
@ -63,10 +59,10 @@ class UploadFileForm(forms.Form):
|
||||
|
||||
|
||||
class MatchFieldForm(forms.Form):
|
||||
""" Step 2 of FileManagementFormView """
|
||||
"""Step 2 of FileManagementFormView."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
"""Setup filemanager and check columsn."""
|
||||
# Get FileManager
|
||||
file_manager = None
|
||||
if 'file_manager' in kwargs:
|
||||
@ -96,10 +92,10 @@ class MatchFieldForm(forms.Form):
|
||||
|
||||
|
||||
class MatchItemForm(forms.Form):
|
||||
""" Step 3 of FileManagementFormView """
|
||||
"""Step 3 of FileManagementFormView."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
"""Setup filemanager and create fields."""
|
||||
# Get FileManager
|
||||
file_manager = None
|
||||
if 'file_manager' in kwargs:
|
||||
@ -194,6 +190,5 @@ class MatchItemForm(forms.Form):
|
||||
)
|
||||
|
||||
def get_special_field(self, col_guess, row, file_manager):
|
||||
""" Function to be overriden in inherited forms to add specific form settings """
|
||||
|
||||
"""Function to be overriden in inherited forms to add specific form settings."""
|
||||
return None
|
||||
|
@ -1,5 +1,5 @@
|
||||
"""
|
||||
Common database model definitions.
|
||||
"""Common database model definitions.
|
||||
|
||||
These models are 'generic' and do not fit a particular business logic object.
|
||||
"""
|
||||
|
||||
@ -42,9 +42,10 @@ logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
class EmptyURLValidator(URLValidator):
|
||||
"""Validator for filed with url - that can be empty."""
|
||||
|
||||
def __call__(self, value):
|
||||
|
||||
"""Make sure empty values pass."""
|
||||
value = str(value).strip()
|
||||
|
||||
if len(value) == 0:
|
||||
@ -55,21 +56,17 @@ class EmptyURLValidator(URLValidator):
|
||||
|
||||
|
||||
class BaseInvenTreeSetting(models.Model):
|
||||
"""
|
||||
An base InvenTreeSetting object is a key:value pair used for storing
|
||||
single values (e.g. one-off settings values).
|
||||
"""
|
||||
"""An base InvenTreeSetting object is a key:value pair used for storing single values (e.g. one-off settings values)."""
|
||||
|
||||
SETTINGS = {}
|
||||
|
||||
class Meta:
|
||||
"""Meta options for BaseInvenTreeSetting -> abstract stops creation of database entry."""
|
||||
|
||||
abstract = True
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""
|
||||
Enforce validation and clean before saving
|
||||
"""
|
||||
|
||||
"""Enforce validation and clean before saving."""
|
||||
self.key = str(self.key).upper()
|
||||
|
||||
self.clean(**kwargs)
|
||||
@ -79,14 +76,12 @@ class BaseInvenTreeSetting(models.Model):
|
||||
|
||||
@classmethod
|
||||
def allValues(cls, user=None, exclude_hidden=False):
|
||||
"""
|
||||
Return a dict of "all" defined global settings.
|
||||
"""Return a dict of "all" defined global settings.
|
||||
|
||||
This performs a single database lookup,
|
||||
and then any settings which are not *in* the database
|
||||
are assigned their default values
|
||||
"""
|
||||
|
||||
results = cls.objects.all()
|
||||
|
||||
# Optionally filter by user
|
||||
@ -131,28 +126,23 @@ class BaseInvenTreeSetting(models.Model):
|
||||
return settings
|
||||
|
||||
def get_kwargs(self):
|
||||
"""
|
||||
Construct kwargs for doing class-based settings lookup,
|
||||
depending on *which* class we are.
|
||||
"""Construct kwargs for doing class-based settings lookup, depending on *which* class we are.
|
||||
|
||||
This is necessary to abtract the settings object
|
||||
from the implementing class (e.g plugins)
|
||||
|
||||
Subclasses should override this function to ensure the kwargs are correctly set.
|
||||
"""
|
||||
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
def get_setting_definition(cls, key, **kwargs):
|
||||
"""
|
||||
Return the 'definition' of a particular settings value, as a dict object.
|
||||
"""Return the 'definition' of a particular settings value, as a dict object.
|
||||
|
||||
- The 'settings' dict can be passed as a kwarg
|
||||
- If not passed, look for cls.SETTINGS
|
||||
- Returns an empty dict if the key is not found
|
||||
"""
|
||||
|
||||
settings = kwargs.get('settings', cls.SETTINGS)
|
||||
|
||||
key = str(key).strip().upper()
|
||||
@ -164,69 +154,56 @@ class BaseInvenTreeSetting(models.Model):
|
||||
|
||||
@classmethod
|
||||
def get_setting_name(cls, key, **kwargs):
|
||||
"""
|
||||
Return the name of a particular setting.
|
||||
"""Return the name of a particular setting.
|
||||
|
||||
If it does not exist, return an empty string.
|
||||
"""
|
||||
|
||||
setting = cls.get_setting_definition(key, **kwargs)
|
||||
return setting.get('name', '')
|
||||
|
||||
@classmethod
|
||||
def get_setting_description(cls, key, **kwargs):
|
||||
"""
|
||||
Return the description for a particular setting.
|
||||
"""Return the description for a particular setting.
|
||||
|
||||
If it does not exist, return an empty string.
|
||||
"""
|
||||
|
||||
setting = cls.get_setting_definition(key, **kwargs)
|
||||
|
||||
return setting.get('description', '')
|
||||
|
||||
@classmethod
|
||||
def get_setting_units(cls, key, **kwargs):
|
||||
"""
|
||||
Return the units for a particular setting.
|
||||
"""Return the units for a particular setting.
|
||||
|
||||
If it does not exist, return an empty string.
|
||||
"""
|
||||
|
||||
setting = cls.get_setting_definition(key, **kwargs)
|
||||
|
||||
return setting.get('units', '')
|
||||
|
||||
@classmethod
|
||||
def get_setting_validator(cls, key, **kwargs):
|
||||
"""
|
||||
Return the validator for a particular setting.
|
||||
"""Return the validator for a particular setting.
|
||||
|
||||
If it does not exist, return None
|
||||
"""
|
||||
|
||||
setting = cls.get_setting_definition(key, **kwargs)
|
||||
|
||||
return setting.get('validator', None)
|
||||
|
||||
@classmethod
|
||||
def get_setting_default(cls, key, **kwargs):
|
||||
"""
|
||||
Return the default value for a particular setting.
|
||||
"""Return the default value for a particular setting.
|
||||
|
||||
If it does not exist, return an empty string
|
||||
"""
|
||||
|
||||
setting = cls.get_setting_definition(key, **kwargs)
|
||||
|
||||
return setting.get('default', '')
|
||||
|
||||
@classmethod
|
||||
def get_setting_choices(cls, key, **kwargs):
|
||||
"""
|
||||
Return the validator choices available for a particular setting.
|
||||
"""
|
||||
|
||||
"""Return the validator choices available for a particular setting."""
|
||||
setting = cls.get_setting_definition(key, **kwargs)
|
||||
|
||||
choices = setting.get('choices', None)
|
||||
@ -239,13 +216,11 @@ class BaseInvenTreeSetting(models.Model):
|
||||
|
||||
@classmethod
|
||||
def get_setting_object(cls, key, **kwargs):
|
||||
"""
|
||||
Return an InvenTreeSetting object matching the given key.
|
||||
"""Return an InvenTreeSetting object matching the given key.
|
||||
|
||||
- Key is case-insensitive
|
||||
- Returns None if no match is made
|
||||
"""
|
||||
|
||||
key = str(key).strip().upper()
|
||||
|
||||
settings = cls.objects.all()
|
||||
@ -311,11 +286,10 @@ class BaseInvenTreeSetting(models.Model):
|
||||
|
||||
@classmethod
|
||||
def get_setting(cls, key, backup_value=None, **kwargs):
|
||||
"""
|
||||
Get the value of a particular setting.
|
||||
"""Get the value of a particular setting.
|
||||
|
||||
If it does not exist, return the backup value (default = None)
|
||||
"""
|
||||
|
||||
# If no backup value is specified, atttempt to retrieve a "default" value
|
||||
if backup_value is None:
|
||||
backup_value = cls.get_setting_default(key, **kwargs)
|
||||
@ -343,9 +317,7 @@ class BaseInvenTreeSetting(models.Model):
|
||||
|
||||
@classmethod
|
||||
def set_setting(cls, key, value, change_user, create=True, **kwargs):
|
||||
"""
|
||||
Set the value of a particular setting.
|
||||
If it does not exist, option to create it.
|
||||
"""Set the value of a particular setting. If it does not exist, option to create it.
|
||||
|
||||
Args:
|
||||
key: settings key
|
||||
@ -353,7 +325,6 @@ class BaseInvenTreeSetting(models.Model):
|
||||
change_user: User object (must be staff member to update a core setting)
|
||||
create: If True, create a new setting if the specified key does not exist.
|
||||
"""
|
||||
|
||||
if change_user is not None and not change_user.is_staff:
|
||||
return
|
||||
|
||||
@ -397,26 +368,26 @@ class BaseInvenTreeSetting(models.Model):
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return name for setting."""
|
||||
return self.__class__.get_setting_name(self.key, **self.get_kwargs())
|
||||
|
||||
@property
|
||||
def default_value(self):
|
||||
"""Return default_value for setting."""
|
||||
return self.__class__.get_setting_default(self.key, **self.get_kwargs())
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
"""Return description for setting."""
|
||||
return self.__class__.get_setting_description(self.key, **self.get_kwargs())
|
||||
|
||||
@property
|
||||
def units(self):
|
||||
"""Return units for setting."""
|
||||
return self.__class__.get_setting_units(self.key, **self.get_kwargs())
|
||||
|
||||
def clean(self, **kwargs):
|
||||
"""
|
||||
If a validator (or multiple validators) are defined for a particular setting key,
|
||||
run them against the 'value' field.
|
||||
"""
|
||||
|
||||
"""If a validator (or multiple validators) are defined for a particular setting key, run them against the 'value' field."""
|
||||
super().clean()
|
||||
|
||||
# Encode as native values
|
||||
@ -437,10 +408,7 @@ class BaseInvenTreeSetting(models.Model):
|
||||
raise ValidationError(_("Chosen value is not a valid option"))
|
||||
|
||||
def run_validator(self, validator):
|
||||
"""
|
||||
Run a validator against the 'value' field for this InvenTreeSetting object.
|
||||
"""
|
||||
|
||||
"""Run a validator against the 'value' field for this InvenTreeSetting object."""
|
||||
if validator is None:
|
||||
return
|
||||
|
||||
@ -485,15 +453,11 @@ class BaseInvenTreeSetting(models.Model):
|
||||
validator(value)
|
||||
|
||||
def validate_unique(self, exclude=None, **kwargs):
|
||||
"""
|
||||
Ensure that the key:value pair is unique.
|
||||
In addition to the base validators, this ensures that the 'key'
|
||||
is unique, using a case-insensitive comparison.
|
||||
"""Ensure that the key:value pair is unique. In addition to the base validators, this ensures that the 'key' is unique, using a case-insensitive comparison.
|
||||
|
||||
Note that sub-classes (UserSetting, PluginSetting) use other filters
|
||||
to determine if the setting is 'unique' or not
|
||||
"""
|
||||
|
||||
super().validate_unique(exclude)
|
||||
|
||||
filters = {
|
||||
@ -520,17 +484,11 @@ class BaseInvenTreeSetting(models.Model):
|
||||
pass
|
||||
|
||||
def choices(self):
|
||||
"""
|
||||
Return the available choices for this setting (or None if no choices are defined)
|
||||
"""
|
||||
|
||||
"""Return the available choices for this setting (or None if no choices are defined)."""
|
||||
return self.__class__.get_setting_choices(self.key, **self.get_kwargs())
|
||||
|
||||
def valid_options(self):
|
||||
"""
|
||||
Return a list of valid options for this setting
|
||||
"""
|
||||
|
||||
"""Return a list of valid options for this setting."""
|
||||
choices = self.choices()
|
||||
|
||||
if not choices:
|
||||
@ -539,21 +497,17 @@ class BaseInvenTreeSetting(models.Model):
|
||||
return [opt[0] for opt in choices]
|
||||
|
||||
def is_choice(self):
|
||||
"""
|
||||
Check if this setting is a "choice" field
|
||||
"""
|
||||
|
||||
"""Check if this setting is a "choice" field."""
|
||||
return self.__class__.get_setting_choices(self.key, **self.get_kwargs()) is not None
|
||||
|
||||
def as_choice(self):
|
||||
"""
|
||||
Render this setting as the "display" value of a choice field,
|
||||
e.g. if the choices are:
|
||||
"""Render this setting as the "display" value of a choice field.
|
||||
|
||||
E.g. if the choices are:
|
||||
[('A4', 'A4 paper'), ('A3', 'A3 paper')],
|
||||
and the value is 'A4',
|
||||
then display 'A4 paper'
|
||||
"""
|
||||
|
||||
choices = self.get_setting_choices(self.key, **self.get_kwargs())
|
||||
|
||||
if not choices:
|
||||
@ -566,30 +520,23 @@ class BaseInvenTreeSetting(models.Model):
|
||||
return self.value
|
||||
|
||||
def is_model(self):
|
||||
"""
|
||||
Check if this setting references a model instance in the database
|
||||
"""
|
||||
|
||||
"""Check if this setting references a model instance in the database."""
|
||||
return self.model_name() is not None
|
||||
|
||||
def model_name(self):
|
||||
"""
|
||||
Return the model name associated with this setting
|
||||
"""
|
||||
|
||||
"""Return the model name associated with this setting."""
|
||||
setting = self.get_setting_definition(self.key, **self.get_kwargs())
|
||||
|
||||
return setting.get('model', None)
|
||||
|
||||
def model_class(self):
|
||||
"""
|
||||
Return the model class associated with this setting, if (and only if):
|
||||
"""Return the model class associated with this setting.
|
||||
|
||||
If (and only if):
|
||||
- It has a defined 'model' parameter
|
||||
- The 'model' parameter is of the form app.model
|
||||
- The 'model' parameter has matches a known app model
|
||||
"""
|
||||
|
||||
model_name = self.model_name()
|
||||
|
||||
if not model_name:
|
||||
@ -617,11 +564,7 @@ class BaseInvenTreeSetting(models.Model):
|
||||
return model
|
||||
|
||||
def api_url(self):
|
||||
"""
|
||||
Return the API url associated with the linked model,
|
||||
if provided, and valid!
|
||||
"""
|
||||
|
||||
"""Return the API url associated with the linked model, if provided, and valid!"""
|
||||
model_class = self.model_class()
|
||||
|
||||
if model_class:
|
||||
@ -634,28 +577,20 @@ class BaseInvenTreeSetting(models.Model):
|
||||
return None
|
||||
|
||||
def is_bool(self):
|
||||
"""
|
||||
Check if this setting is required to be a boolean value
|
||||
"""
|
||||
|
||||
"""Check if this setting is required to be a boolean value."""
|
||||
validator = self.__class__.get_setting_validator(self.key, **self.get_kwargs())
|
||||
|
||||
return self.__class__.validator_is_bool(validator)
|
||||
|
||||
def as_bool(self):
|
||||
"""
|
||||
Return the value of this setting converted to a boolean value.
|
||||
"""Return the value of this setting converted to a boolean value.
|
||||
|
||||
Warning: Only use on values where is_bool evaluates to true!
|
||||
"""
|
||||
|
||||
return InvenTree.helpers.str2bool(self.value)
|
||||
|
||||
def setting_type(self):
|
||||
"""
|
||||
Return the field type identifier for this setting object
|
||||
"""
|
||||
|
||||
"""Return the field type identifier for this setting object."""
|
||||
if self.is_bool():
|
||||
return 'boolean'
|
||||
|
||||
@ -670,7 +605,7 @@ class BaseInvenTreeSetting(models.Model):
|
||||
|
||||
@classmethod
|
||||
def validator_is_bool(cls, validator):
|
||||
|
||||
"""Return if validator is for bool."""
|
||||
if validator == bool:
|
||||
return True
|
||||
|
||||
@ -682,17 +617,14 @@ class BaseInvenTreeSetting(models.Model):
|
||||
return False
|
||||
|
||||
def is_int(self,):
|
||||
"""
|
||||
Check if the setting is required to be an integer value:
|
||||
"""
|
||||
|
||||
"""Check if the setting is required to be an integer value."""
|
||||
validator = self.__class__.get_setting_validator(self.key, **self.get_kwargs())
|
||||
|
||||
return self.__class__.validator_is_int(validator)
|
||||
|
||||
@classmethod
|
||||
def validator_is_int(cls, validator):
|
||||
|
||||
"""Return if validator is for int."""
|
||||
if validator == int:
|
||||
return True
|
||||
|
||||
@ -704,12 +636,10 @@ class BaseInvenTreeSetting(models.Model):
|
||||
return False
|
||||
|
||||
def as_int(self):
|
||||
"""
|
||||
Return the value of this setting converted to a boolean value.
|
||||
"""Return the value of this setting converted to a boolean value.
|
||||
|
||||
If an error occurs, return the default value
|
||||
"""
|
||||
|
||||
try:
|
||||
value = int(self.value)
|
||||
except (ValueError, TypeError):
|
||||
@ -719,41 +649,34 @@ class BaseInvenTreeSetting(models.Model):
|
||||
|
||||
@classmethod
|
||||
def is_protected(cls, key, **kwargs):
|
||||
"""
|
||||
Check if the setting value is protected
|
||||
"""
|
||||
|
||||
"""Check if the setting value is protected."""
|
||||
setting = cls.get_setting_definition(key, **kwargs)
|
||||
|
||||
return setting.get('protected', False)
|
||||
|
||||
@property
|
||||
def protected(self):
|
||||
"""Returns if setting is protected from rendering."""
|
||||
return self.__class__.is_protected(self.key, **self.get_kwargs())
|
||||
|
||||
|
||||
def settings_group_options():
|
||||
"""
|
||||
Build up group tuple for settings based on your choices
|
||||
"""
|
||||
"""Build up group tuple for settings based on your choices."""
|
||||
return [('', _('No group')), *[(str(a.id), str(a)) for a in Group.objects.all()]]
|
||||
|
||||
|
||||
class InvenTreeSetting(BaseInvenTreeSetting):
|
||||
"""
|
||||
An InvenTreeSetting object is a key:value pair used for storing
|
||||
single values (e.g. one-off settings values).
|
||||
"""An InvenTreeSetting object is a key:value pair used for storing single values (e.g. one-off settings values).
|
||||
|
||||
The class provides a way of retrieving the value for a particular key,
|
||||
even if that key does not exist.
|
||||
"""
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""
|
||||
When saving a global setting, check to see if it requires a server restart.
|
||||
"""When saving a global setting, check to see if it requires a server restart.
|
||||
|
||||
If so, set the "SERVER_RESTART_REQUIRED" setting to True
|
||||
"""
|
||||
|
||||
super().save()
|
||||
|
||||
if self.requires_restart():
|
||||
@ -1235,6 +1158,8 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
||||
}
|
||||
|
||||
class Meta:
|
||||
"""Meta options for InvenTreeSetting."""
|
||||
|
||||
verbose_name = "InvenTree Setting"
|
||||
verbose_name_plural = "InvenTree Settings"
|
||||
|
||||
@ -1246,18 +1171,11 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
||||
)
|
||||
|
||||
def to_native_value(self):
|
||||
"""
|
||||
Return the "pythonic" value,
|
||||
e.g. convert "True" to True, and "1" to 1
|
||||
"""
|
||||
|
||||
"""Return the "pythonic" value, e.g. convert "True" to True, and "1" to 1."""
|
||||
return self.__class__.get_setting(self.key)
|
||||
|
||||
def requires_restart(self):
|
||||
"""
|
||||
Return True if this setting requires a server restart after changing
|
||||
"""
|
||||
|
||||
"""Return True if this setting requires a server restart after changing."""
|
||||
options = InvenTreeSetting.SETTINGS.get(self.key, None)
|
||||
|
||||
if options:
|
||||
@ -1267,9 +1185,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
||||
|
||||
|
||||
class InvenTreeUserSetting(BaseInvenTreeSetting):
|
||||
"""
|
||||
An InvenTreeSetting object with a usercontext
|
||||
"""
|
||||
"""An InvenTreeSetting object with a usercontext."""
|
||||
|
||||
SETTINGS = {
|
||||
'HOMEPAGE_PART_STARRED': {
|
||||
@ -1561,6 +1477,8 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
|
||||
}
|
||||
|
||||
class Meta:
|
||||
"""Meta options for InvenTreeUserSetting."""
|
||||
|
||||
verbose_name = "InvenTree User Setting"
|
||||
verbose_name_plural = "InvenTree User Settings"
|
||||
constraints = [
|
||||
@ -1584,36 +1502,30 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
|
||||
|
||||
@classmethod
|
||||
def get_setting_object(cls, key, user=None):
|
||||
"""Return setting object for provided user."""
|
||||
return super().get_setting_object(key, user=user)
|
||||
|
||||
def validate_unique(self, exclude=None, **kwargs):
|
||||
"""Return if the setting (including key) is unique."""
|
||||
return super().validate_unique(exclude=exclude, user=self.user)
|
||||
|
||||
def to_native_value(self):
|
||||
"""
|
||||
Return the "pythonic" value,
|
||||
e.g. convert "True" to True, and "1" to 1
|
||||
"""
|
||||
|
||||
"""Return the "pythonic" value, e.g. convert "True" to True, and "1" to 1."""
|
||||
return self.__class__.get_setting(self.key, user=self.user)
|
||||
|
||||
def get_kwargs(self):
|
||||
"""
|
||||
Explicit kwargs required to uniquely identify a particular setting object,
|
||||
in addition to the 'key' parameter
|
||||
"""
|
||||
|
||||
"""Explicit kwargs required to uniquely identify a particular setting object, in addition to the 'key' parameter."""
|
||||
return {
|
||||
'user': self.user,
|
||||
}
|
||||
|
||||
|
||||
class PriceBreak(models.Model):
|
||||
"""
|
||||
Represents a PriceBreak model
|
||||
"""
|
||||
"""Represents a PriceBreak model."""
|
||||
|
||||
class Meta:
|
||||
"""Define this as abstract -> no DB entry is created."""
|
||||
|
||||
abstract = True
|
||||
|
||||
quantity = InvenTree.fields.RoundingDecimalField(
|
||||
@ -1634,13 +1546,11 @@ class PriceBreak(models.Model):
|
||||
)
|
||||
|
||||
def convert_to(self, currency_code):
|
||||
"""
|
||||
Convert the unit-price at this price break to the specified currency code.
|
||||
"""Convert the unit-price at this price break to the specified currency code.
|
||||
|
||||
Args:
|
||||
currency_code - The currency code to convert to (e.g "USD" or "AUD")
|
||||
currency_code: The currency code to convert to (e.g "USD" or "AUD")
|
||||
"""
|
||||
|
||||
try:
|
||||
converted = convert_money(self.price, currency_code)
|
||||
except MissingRate:
|
||||
@ -1651,7 +1561,7 @@ class PriceBreak(models.Model):
|
||||
|
||||
|
||||
def get_price(instance, quantity, moq=True, multiples=True, currency=None, break_name: str = 'price_breaks'):
|
||||
""" Calculate the price based on quantity price breaks.
|
||||
"""Calculate the price based on quantity price breaks.
|
||||
|
||||
- Don't forget to add in flat-fee cost (base_cost field)
|
||||
- If MOQ (minimum order quantity) is required, bump quantity
|
||||
@ -1721,7 +1631,7 @@ def get_price(instance, quantity, moq=True, multiples=True, currency=None, break
|
||||
|
||||
|
||||
class ColorTheme(models.Model):
|
||||
""" Color Theme Setting """
|
||||
"""Color Theme Setting."""
|
||||
name = models.CharField(max_length=20,
|
||||
default='',
|
||||
blank=True)
|
||||
@ -1731,7 +1641,7 @@ class ColorTheme(models.Model):
|
||||
|
||||
@classmethod
|
||||
def get_color_themes_choices(cls):
|
||||
""" Get all color themes from static folder """
|
||||
"""Get all color themes from static folder."""
|
||||
if settings.TESTING and not os.path.exists(settings.STATIC_COLOR_THEMES_DIR):
|
||||
logger.error('Theme directory does not exsist')
|
||||
return []
|
||||
@ -1750,7 +1660,7 @@ class ColorTheme(models.Model):
|
||||
|
||||
@classmethod
|
||||
def is_valid_choice(cls, user_color_theme):
|
||||
""" Check if color theme is valid choice """
|
||||
"""Check if color theme is valid choice."""
|
||||
try:
|
||||
user_color_theme_name = user_color_theme.name
|
||||
except AttributeError:
|
||||
@ -1764,13 +1674,15 @@ class ColorTheme(models.Model):
|
||||
|
||||
|
||||
class VerificationMethod:
|
||||
"""Class to hold method references."""
|
||||
|
||||
NONE = 0
|
||||
TOKEN = 1
|
||||
HMAC = 2
|
||||
|
||||
|
||||
class WebhookEndpoint(models.Model):
|
||||
""" Defines a Webhook entdpoint
|
||||
"""Defines a Webhook entdpoint.
|
||||
|
||||
Attributes:
|
||||
endpoint_id: Path to the webhook,
|
||||
@ -1835,9 +1747,19 @@ class WebhookEndpoint(models.Model):
|
||||
# To be overridden
|
||||
|
||||
def init(self, request, *args, **kwargs):
|
||||
"""Set verification method.
|
||||
|
||||
Args:
|
||||
request: Original request object.
|
||||
"""
|
||||
self.verify = self.VERIFICATION_METHOD
|
||||
|
||||
def process_webhook(self):
|
||||
"""Process the webhook incomming.
|
||||
|
||||
This does not deal with the data itself - that happens in process_payload.
|
||||
Do not touch or pickle data here - it was not verified to be safe.
|
||||
"""
|
||||
if self.token:
|
||||
self.verify = VerificationMethod.TOKEN
|
||||
if self.secret:
|
||||
@ -1845,6 +1767,10 @@ class WebhookEndpoint(models.Model):
|
||||
return True
|
||||
|
||||
def validate_token(self, payload, headers, request):
|
||||
"""Make sure that the provided token (if any) confirms to the setting for this endpoint.
|
||||
|
||||
This can be overridden to create your own token validation method.
|
||||
"""
|
||||
token = headers.get(self.TOKEN_NAME, "")
|
||||
|
||||
# no token
|
||||
@ -1866,7 +1792,14 @@ class WebhookEndpoint(models.Model):
|
||||
|
||||
return True
|
||||
|
||||
def save_data(self, payload, headers=None, request=None):
|
||||
def save_data(self, payload=None, headers=None, request=None):
|
||||
"""Safes payload to database.
|
||||
|
||||
Args:
|
||||
payload (optional): Payload that was send along. Defaults to None.
|
||||
headers (optional): Headers that were send along. Defaults to None.
|
||||
request (optional): Original request object. Defaults to None.
|
||||
"""
|
||||
return WebhookMessage.objects.create(
|
||||
host=request.get_host(),
|
||||
header=json.dumps({key: val for key, val in headers.items()}),
|
||||
@ -1874,15 +1807,35 @@ class WebhookEndpoint(models.Model):
|
||||
endpoint=self,
|
||||
)
|
||||
|
||||
def process_payload(self, message, payload=None, headers=None):
|
||||
def process_payload(self, message, payload=None, headers=None) -> bool:
|
||||
"""Process a payload.
|
||||
|
||||
Args:
|
||||
message: DB entry for this message mm
|
||||
payload (optional): Payload that was send along. Defaults to None.
|
||||
headers (optional): Headers that were included. Defaults to None.
|
||||
|
||||
Returns:
|
||||
bool: Was the message processed
|
||||
"""
|
||||
return True
|
||||
|
||||
def get_return(self, payload, headers=None, request=None):
|
||||
def get_return(self, payload=None, headers=None, request=None) -> str:
|
||||
"""Returns the message that should be returned to the endpoint caller.
|
||||
|
||||
Args:
|
||||
payload (optional): Payload that was send along. Defaults to None.
|
||||
headers (optional): Headers that were send along. Defaults to None.
|
||||
request (optional): Original request object. Defaults to None.
|
||||
|
||||
Returns:
|
||||
str: Message for caller.
|
||||
"""
|
||||
return self.MESSAGE_OK
|
||||
|
||||
|
||||
class WebhookMessage(models.Model):
|
||||
""" Defines a webhook message
|
||||
"""Defines a webhook message.
|
||||
|
||||
Attributes:
|
||||
message_id: Unique identifier for this message,
|
||||
@ -1939,8 +1892,7 @@ class WebhookMessage(models.Model):
|
||||
|
||||
|
||||
class NotificationEntry(models.Model):
|
||||
"""
|
||||
A NotificationEntry records the last time a particular notifaction was sent out.
|
||||
"""A NotificationEntry records the last time a particular notifaction was sent out.
|
||||
|
||||
It is recorded to ensure that notifications are not sent out "too often" to users.
|
||||
|
||||
@ -1951,6 +1903,8 @@ class NotificationEntry(models.Model):
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
"""Meta options for NotificationEntry."""
|
||||
|
||||
unique_together = [
|
||||
('key', 'uid'),
|
||||
]
|
||||
@ -1970,10 +1924,7 @@ class NotificationEntry(models.Model):
|
||||
|
||||
@classmethod
|
||||
def check_recent(cls, key: str, uid: int, delta: timedelta):
|
||||
"""
|
||||
Test if a particular notification has been sent in the specified time period
|
||||
"""
|
||||
|
||||
"""Test if a particular notification has been sent in the specified time period."""
|
||||
since = datetime.now().date() - delta
|
||||
|
||||
entries = cls.objects.filter(
|
||||
@ -1986,10 +1937,7 @@ class NotificationEntry(models.Model):
|
||||
|
||||
@classmethod
|
||||
def notify(cls, key: str, uid: int):
|
||||
"""
|
||||
Notify the database that a particular notification has been sent out
|
||||
"""
|
||||
|
||||
"""Notify the database that a particular notification has been sent out."""
|
||||
entry, created = cls.objects.get_or_create(
|
||||
key=key,
|
||||
uid=uid
|
||||
@ -1999,8 +1947,7 @@ class NotificationEntry(models.Model):
|
||||
|
||||
|
||||
class NotificationMessage(models.Model):
|
||||
"""
|
||||
A NotificationEntry records the last time a particular notifaction was sent out.
|
||||
"""A NotificationEntry records the last time a particular notifaction was sent out.
|
||||
|
||||
It is recorded to ensure that notifications are not sent out "too often" to users.
|
||||
|
||||
@ -2073,13 +2020,14 @@ class NotificationMessage(models.Model):
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return API endpoint."""
|
||||
return reverse('api-notifications-list')
|
||||
|
||||
def age(self):
|
||||
"""age of the message in seconds"""
|
||||
"""Age of the message in seconds."""
|
||||
delta = now() - self.creation
|
||||
return delta.seconds
|
||||
|
||||
def age_human(self):
|
||||
"""humanized age"""
|
||||
"""Humanized age."""
|
||||
return naturaltime(self.creation)
|
||||
|
@ -1,3 +1,5 @@
|
||||
"""Base classes and functions for notifications."""
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
@ -12,9 +14,7 @@ logger = logging.getLogger('inventree')
|
||||
|
||||
# region methods
|
||||
class NotificationMethod:
|
||||
"""
|
||||
Base class for notification methods
|
||||
"""
|
||||
"""Base class for notification methods."""
|
||||
|
||||
METHOD_NAME = ''
|
||||
METHOD_ICON = None
|
||||
@ -24,6 +24,13 @@ class NotificationMethod:
|
||||
USER_SETTING = None
|
||||
|
||||
def __init__(self, obj, category, targets, context) -> None:
|
||||
"""Check that the method is read.
|
||||
|
||||
This checks that:
|
||||
- All needed functions are implemented
|
||||
- The method is not disabled via plugin
|
||||
- All needed contaxt values were provided
|
||||
"""
|
||||
# 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')
|
||||
@ -47,6 +54,7 @@ class NotificationMethod:
|
||||
self.targets = self.get_targets()
|
||||
|
||||
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 accesible so we are on the end
|
||||
if not isinstance(obj, (list, dict, tuple, )):
|
||||
@ -82,21 +90,33 @@ class NotificationMethod:
|
||||
return context
|
||||
|
||||
def get_targets(self):
|
||||
"""Returns targets for notifications.
|
||||
|
||||
Processes `self.targets` to extract all users that should be notified.
|
||||
"""
|
||||
raise NotImplementedError('The `get_targets` method must be implemented!')
|
||||
|
||||
def setup(self):
|
||||
"""Set up context before notifications are send.
|
||||
|
||||
This is intended to be overridden in method implementations.
|
||||
"""
|
||||
return True
|
||||
|
||||
def cleanup(self):
|
||||
"""Clean up context after all notifications were send.
|
||||
|
||||
This is intended to be overridden in method implementations.
|
||||
"""
|
||||
return True
|
||||
|
||||
# region plugins
|
||||
def get_plugin(self):
|
||||
"""Returns plugin class"""
|
||||
"""Returns plugin class."""
|
||||
return False
|
||||
|
||||
def global_setting_disable(self):
|
||||
"""Check if the method is defined in a plugin and has a global setting"""
|
||||
"""Check if the method is defined in a plugin and has a global setting."""
|
||||
# Check if plugin has a setting
|
||||
if not self.GLOBAL_SETTING:
|
||||
return False
|
||||
@ -115,29 +135,45 @@ class NotificationMethod:
|
||||
return False
|
||||
|
||||
def usersetting(self, target):
|
||||
"""
|
||||
Returns setting for this method for a given user
|
||||
"""
|
||||
"""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)
|
||||
# endregion
|
||||
|
||||
|
||||
class SingleNotificationMethod(NotificationMethod):
|
||||
"""NotificationMethod that sends notifications one by one."""
|
||||
|
||||
def send(self, target):
|
||||
"""This function must be overriden."""
|
||||
raise NotImplementedError('The `send` method must be overriden!')
|
||||
|
||||
|
||||
class BulkNotificationMethod(NotificationMethod):
|
||||
"""NotificationMethod that sends all notifications in bulk."""
|
||||
|
||||
def send_bulk(self):
|
||||
"""This function must be overriden."""
|
||||
raise NotImplementedError('The `send` method must be overriden!')
|
||||
# endregion
|
||||
|
||||
|
||||
class MethodStorageClass:
|
||||
"""Class that works as registry for all available notification methods in InvenTree.
|
||||
|
||||
Is initialized on startup as one instance named `storage` in this file.
|
||||
"""
|
||||
|
||||
liste = None
|
||||
user_settings = {}
|
||||
|
||||
def collect(self, selected_classes=None):
|
||||
"""Collect all classes in the enviroment that are notification methods.
|
||||
|
||||
Can be filtered to only include provided classes for testing.
|
||||
|
||||
Args:
|
||||
selected_classes (class, optional): References to the classes that should be registered. Defaults to None.
|
||||
"""
|
||||
logger.info('collecting notification methods')
|
||||
current_method = inheritors(NotificationMethod) - IGNORED_NOTIFICATION_CLS
|
||||
|
||||
@ -155,7 +191,17 @@ class MethodStorageClass:
|
||||
storage.liste = list(filtered_list.values())
|
||||
logger.info(f'found {len(storage.liste)} notification methods')
|
||||
|
||||
def get_usersettings(self, user):
|
||||
def get_usersettings(self, user) -> list:
|
||||
"""Returns all user settings for a specific user.
|
||||
|
||||
This is needed to show them in the settings UI.
|
||||
|
||||
Args:
|
||||
user (User): User that should be used as a filter.
|
||||
|
||||
Returns:
|
||||
list: All applicablae notification settings.
|
||||
"""
|
||||
methods = []
|
||||
for item in storage.liste:
|
||||
if item.USER_SETTING:
|
||||
@ -186,12 +232,16 @@ storage = MethodStorageClass()
|
||||
|
||||
|
||||
class UIMessageNotification(SingleNotificationMethod):
|
||||
"""Delivery method for sending specific users notifications in the notification pain in the web UI."""
|
||||
|
||||
METHOD_NAME = 'ui_message'
|
||||
|
||||
def get_targets(self):
|
||||
"""Just return the targets - no tricks here."""
|
||||
return self.targets
|
||||
|
||||
def send(self, target):
|
||||
"""Send a UI notification to a user."""
|
||||
NotificationMessage.objects.create(
|
||||
target_object=self.obj,
|
||||
source_object=target,
|
||||
@ -204,10 +254,7 @@ class UIMessageNotification(SingleNotificationMethod):
|
||||
|
||||
|
||||
def trigger_notification(obj, category=None, obj_ref='pk', **kwargs):
|
||||
"""
|
||||
Send out a notification
|
||||
"""
|
||||
|
||||
"""Send out a notification."""
|
||||
targets = kwargs.get('targets', None)
|
||||
target_fnc = kwargs.get('target_fnc', None)
|
||||
target_args = kwargs.get('target_args', [])
|
||||
@ -267,6 +314,15 @@ def trigger_notification(obj, category=None, obj_ref='pk', **kwargs):
|
||||
|
||||
|
||||
def deliver_notification(cls: NotificationMethod, obj, category: str, targets, context: dict):
|
||||
"""Send notification with the provided class.
|
||||
|
||||
This:
|
||||
- Intis the method
|
||||
- Checks that there are valid targets
|
||||
- Runs the delivery setup
|
||||
- Sends notifications either via `send_bulk` or send`
|
||||
- Runs the delivery cleanup
|
||||
"""
|
||||
# Init delivery method
|
||||
method = cls(obj, category, targets, context)
|
||||
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
JSON serializers for common components
|
||||
"""
|
||||
"""JSON serializers for common components."""
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
@ -11,9 +9,7 @@ from InvenTree.serializers import InvenTreeModelSerializer
|
||||
|
||||
|
||||
class SettingsSerializer(InvenTreeModelSerializer):
|
||||
"""
|
||||
Base serializer for a settings object
|
||||
"""
|
||||
"""Base serializer for a settings object."""
|
||||
|
||||
key = serializers.CharField(read_only=True)
|
||||
|
||||
@ -30,10 +26,7 @@ class SettingsSerializer(InvenTreeModelSerializer):
|
||||
api_url = serializers.CharField(read_only=True)
|
||||
|
||||
def get_choices(self, obj):
|
||||
"""
|
||||
Returns the choices available for a given item
|
||||
"""
|
||||
|
||||
"""Returns the choices available for a given item."""
|
||||
results = []
|
||||
|
||||
choices = obj.choices()
|
||||
@ -48,10 +41,7 @@ class SettingsSerializer(InvenTreeModelSerializer):
|
||||
return results
|
||||
|
||||
def get_value(self, obj):
|
||||
"""
|
||||
Make sure protected values are not returned
|
||||
"""
|
||||
|
||||
"""Make sure protected values are not returned."""
|
||||
# never return protected values
|
||||
if obj.protected:
|
||||
result = '***'
|
||||
@ -62,11 +52,11 @@ class SettingsSerializer(InvenTreeModelSerializer):
|
||||
|
||||
|
||||
class GlobalSettingsSerializer(SettingsSerializer):
|
||||
"""
|
||||
Serializer for the InvenTreeSetting model
|
||||
"""
|
||||
"""Serializer for the InvenTreeSetting model."""
|
||||
|
||||
class Meta:
|
||||
"""Meta options for GlobalSettingsSerializer."""
|
||||
|
||||
model = InvenTreeSetting
|
||||
fields = [
|
||||
'pk',
|
||||
@ -82,13 +72,13 @@ class GlobalSettingsSerializer(SettingsSerializer):
|
||||
|
||||
|
||||
class UserSettingsSerializer(SettingsSerializer):
|
||||
"""
|
||||
Serializer for the InvenTreeUserSetting model
|
||||
"""
|
||||
"""Serializer for the InvenTreeUserSetting model."""
|
||||
|
||||
user = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
"""Meta options for UserSettingsSerializer."""
|
||||
|
||||
model = InvenTreeUserSetting
|
||||
fields = [
|
||||
'pk',
|
||||
@ -105,8 +95,7 @@ class UserSettingsSerializer(SettingsSerializer):
|
||||
|
||||
|
||||
class GenericReferencedSettingSerializer(SettingsSerializer):
|
||||
"""
|
||||
Serializer for a GenericReferencedSetting model
|
||||
"""Serializer for a GenericReferencedSetting model.
|
||||
|
||||
Args:
|
||||
MODEL: model class for the serializer
|
||||
@ -118,9 +107,9 @@ class GenericReferencedSettingSerializer(SettingsSerializer):
|
||||
EXTRA_FIELDS = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Init overrides the Meta class to make it dynamic"""
|
||||
"""Init overrides the Meta class to make it dynamic."""
|
||||
class CustomMeta:
|
||||
"""Scaffold for custom Meta class"""
|
||||
"""Scaffold for custom Meta class."""
|
||||
fields = [
|
||||
'pk',
|
||||
'key',
|
||||
@ -144,9 +133,7 @@ class GenericReferencedSettingSerializer(SettingsSerializer):
|
||||
|
||||
|
||||
class NotificationMessageSerializer(InvenTreeModelSerializer):
|
||||
"""
|
||||
Serializer for the InvenTreeUserSetting model
|
||||
"""
|
||||
"""Serializer for the InvenTreeUserSetting model."""
|
||||
|
||||
target = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
@ -169,12 +156,16 @@ class NotificationMessageSerializer(InvenTreeModelSerializer):
|
||||
read = serializers.BooleanField(read_only=True)
|
||||
|
||||
def get_target(self, obj):
|
||||
"""Function to resolve generic object reference to target."""
|
||||
return get_objectreference(obj, 'target_content_type', 'target_object_id')
|
||||
|
||||
def get_source(self, obj):
|
||||
"""Function to resolve generic object reference to source."""
|
||||
return get_objectreference(obj, 'source_content_type', 'source_object_id')
|
||||
|
||||
class Meta:
|
||||
"""Meta options for NotificationMessageSerializer."""
|
||||
|
||||
model = NotificationMessage
|
||||
fields = [
|
||||
'pk',
|
||||
@ -192,8 +183,10 @@ class NotificationMessageSerializer(InvenTreeModelSerializer):
|
||||
|
||||
|
||||
class NotificationReadSerializer(NotificationMessageSerializer):
|
||||
"""Serializer for reading a notification."""
|
||||
|
||||
def is_valid(self, raise_exception=False):
|
||||
"""Ensure instance data is available for view and let validation pass."""
|
||||
self.instance = self.context['instance'] # set instance that should be returned
|
||||
self._validated_data = True
|
||||
return True
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
User-configurable settings for the common app
|
||||
"""
|
||||
"""User-configurable settings for the common app."""
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
@ -8,9 +6,7 @@ from moneyed import CURRENCIES
|
||||
|
||||
|
||||
def currency_code_default():
|
||||
"""
|
||||
Returns the default currency code (or USD if not specified)
|
||||
"""
|
||||
"""Returns the default currency code (or USD if not specified)"""
|
||||
from django.db.utils import ProgrammingError
|
||||
|
||||
from common.models import InvenTreeSetting
|
||||
@ -28,23 +24,17 @@ def currency_code_default():
|
||||
|
||||
|
||||
def currency_code_mappings():
|
||||
"""
|
||||
Returns the current currency choices
|
||||
"""
|
||||
"""Returns the current currency choices."""
|
||||
return [(a, CURRENCIES[a].name) for a in settings.CURRENCIES]
|
||||
|
||||
|
||||
def currency_codes():
|
||||
"""
|
||||
Returns the current currency codes
|
||||
"""
|
||||
"""Returns the current currency codes."""
|
||||
return [a for a in settings.CURRENCIES]
|
||||
|
||||
|
||||
def stock_expiry_enabled():
|
||||
"""
|
||||
Returns True if the stock expiry feature is enabled
|
||||
"""
|
||||
"""Returns True if the stock expiry feature is enabled."""
|
||||
from common.models import InvenTreeSetting
|
||||
|
||||
return InvenTreeSetting.get_setting('STOCK_ENABLE_EXPIRY')
|
||||
|
@ -1,3 +1,5 @@
|
||||
"""Tasks (processes that get offloaded) for common app."""
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
@ -7,12 +9,10 @@ logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
def delete_old_notifications():
|
||||
"""
|
||||
Remove old notifications from the database.
|
||||
"""Remove old notifications from the database.
|
||||
|
||||
Anything older than ~3 months is removed
|
||||
"""
|
||||
|
||||
try:
|
||||
from common.models import NotificationEntry
|
||||
except AppRegistryNotReady: # pragma: no cover
|
||||
|
@ -1,3 +1,5 @@
|
||||
"""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)
|
||||
@ -6,9 +8,10 @@ from plugin.models import NotificationUserSetting
|
||||
|
||||
|
||||
class BaseNotificationTests(BaseNotificationIntegrationTest):
|
||||
"""Tests for basic NotificationMethod."""
|
||||
|
||||
def test_NotificationMethod(self):
|
||||
"""ensure the implementation requirements are tested"""
|
||||
"""Ensure the implementation requirements are tested."""
|
||||
|
||||
class FalseNotificationMethod(NotificationMethod):
|
||||
METHOD_NAME = 'FalseNotification'
|
||||
@ -17,12 +20,12 @@ class BaseNotificationTests(BaseNotificationIntegrationTest):
|
||||
METHOD_NAME = 'AnotherFalseNotification'
|
||||
|
||||
def send(self):
|
||||
"""a comment so we do not need a pass"""
|
||||
"""A comment so we do not need a pass."""
|
||||
|
||||
class NoNameNotificationMethod(NotificationMethod):
|
||||
|
||||
def send(self):
|
||||
"""a comment so we do not need a pass"""
|
||||
"""A comment so we do not need a pass."""
|
||||
|
||||
class WrongContextNotificationMethod(NotificationMethod):
|
||||
METHOD_NAME = 'WrongContextNotification'
|
||||
@ -34,7 +37,7 @@ class BaseNotificationTests(BaseNotificationIntegrationTest):
|
||||
]
|
||||
|
||||
def send(self):
|
||||
"""a comment so we do not need a pass"""
|
||||
"""A comment so we do not need a pass."""
|
||||
|
||||
# no send / send bulk
|
||||
with self.assertRaises(NotImplementedError):
|
||||
@ -53,11 +56,12 @@ class BaseNotificationTests(BaseNotificationIntegrationTest):
|
||||
AnotherFalseNotificationMethod('', '', '', {'name': 1, 'message': 2, }, )
|
||||
|
||||
def test_failing_passing(self):
|
||||
"""Ensure that an error in one deliverymethod is not blocking all mehthods."""
|
||||
# cover failing delivery
|
||||
self._notification_run()
|
||||
|
||||
def test_errors_passing(self):
|
||||
"""ensure that errors do not kill the whole delivery"""
|
||||
"""Ensure that errors do not kill the whole delivery."""
|
||||
|
||||
class ErrorImplementation(SingleNotificationMethod):
|
||||
METHOD_NAME = 'ErrorImplementation'
|
||||
@ -72,10 +76,14 @@ class BaseNotificationTests(BaseNotificationIntegrationTest):
|
||||
|
||||
|
||||
class BulkNotificationMethodTests(BaseNotificationIntegrationTest):
|
||||
"""Tests for BulkNotificationMethod classes specifically.
|
||||
|
||||
General tests for NotificationMethods are in BaseNotificationTests.
|
||||
"""
|
||||
|
||||
def test_BulkNotificationMethod(self):
|
||||
"""
|
||||
Ensure the implementation requirements are tested.
|
||||
"""Ensure the implementation requirements are tested.
|
||||
|
||||
MixinNotImplementedError needs to raise if the send_bulk() method is not set.
|
||||
"""
|
||||
|
||||
@ -90,10 +98,14 @@ class BulkNotificationMethodTests(BaseNotificationIntegrationTest):
|
||||
|
||||
|
||||
class SingleNotificationMethodTests(BaseNotificationIntegrationTest):
|
||||
"""Tests for SingleNotificationMethod classes specifically.
|
||||
|
||||
General tests for NotificationMethods are in BaseNotificationTests.
|
||||
"""
|
||||
|
||||
def test_SingleNotificationMethod(self):
|
||||
"""
|
||||
Ensure the implementation requirements are tested.
|
||||
"""Ensure the implementation requirements are tested.
|
||||
|
||||
MixinNotImplementedError needs to raise if the send() method is not set.
|
||||
"""
|
||||
|
||||
@ -110,14 +122,15 @@ class SingleNotificationMethodTests(BaseNotificationIntegrationTest):
|
||||
|
||||
|
||||
class NotificationUserSettingTests(BaseNotificationIntegrationTest):
|
||||
""" Tests for NotificationUserSetting """
|
||||
"""Tests for NotificationUserSetting."""
|
||||
|
||||
def setUp(self):
|
||||
"""Setup for all tests."""
|
||||
super().setUp()
|
||||
self.client.login(username=self.user.username, password='password')
|
||||
|
||||
def test_setting_attributes(self):
|
||||
"""check notification method plugin methods: usersettings and tags """
|
||||
"""Check notification method plugin methods: usersettings and tags."""
|
||||
|
||||
class SampleImplementation(BulkNotificationMethod):
|
||||
METHOD_NAME = 'test'
|
||||
|
@ -1,4 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests for tasks in app common."""
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from common.models import NotificationEntry
|
||||
@ -8,12 +9,10 @@ from . import tasks as common_tasks
|
||||
|
||||
|
||||
class TaskTest(TestCase):
|
||||
"""
|
||||
Tests for common tasks
|
||||
"""
|
||||
"""Tests for common tasks."""
|
||||
|
||||
def test_delete(self):
|
||||
|
||||
"""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,)
|
||||
|
@ -1,3 +1 @@
|
||||
"""
|
||||
Unit tests for the views associated with the 'common' app
|
||||
"""
|
||||
"""Unit tests for the views associated with the 'common' app."""
|
||||
|
@ -1,3 +1,4 @@
|
||||
"""Tests for mechanisms in common."""
|
||||
|
||||
import json
|
||||
from datetime import timedelta
|
||||
@ -19,16 +20,14 @@ CONTENT_TYPE_JSON = 'application/json'
|
||||
|
||||
|
||||
class SettingsTest(InvenTreeTestCase):
|
||||
"""
|
||||
Tests for the 'settings' model
|
||||
"""
|
||||
"""Tests for the 'settings' model."""
|
||||
|
||||
fixtures = [
|
||||
'settings',
|
||||
]
|
||||
|
||||
def test_settings_objects(self):
|
||||
|
||||
"""Test fixture loading and lookup for settings."""
|
||||
# There should be two settings objects in the database
|
||||
settings = InvenTreeSetting.objects.all()
|
||||
|
||||
@ -42,9 +41,7 @@ class SettingsTest(InvenTreeTestCase):
|
||||
self.assertEqual(InvenTreeSetting.get_setting_object('iNvEnTrEE_inSTanCE').pk, 1)
|
||||
|
||||
def test_settings_functions(self):
|
||||
"""
|
||||
Test settings functions and properties
|
||||
"""
|
||||
"""Test settings functions and properties."""
|
||||
# define settings to check
|
||||
instance_ref = 'INVENTREE_INSTANCE'
|
||||
instance_obj = InvenTreeSetting.get_setting_object(instance_ref)
|
||||
@ -90,9 +87,7 @@ class SettingsTest(InvenTreeTestCase):
|
||||
self.assertEqual(stale_days.to_native_value(), 0)
|
||||
|
||||
def test_allValues(self):
|
||||
"""
|
||||
Make sure that the allValues functions returns correctly
|
||||
"""
|
||||
"""Make sure that the allValues functions returns correctly."""
|
||||
# define testing settings
|
||||
|
||||
# check a few keys
|
||||
@ -103,7 +98,13 @@ class SettingsTest(InvenTreeTestCase):
|
||||
self.assertIn('SIGNUP_GROUP', result)
|
||||
|
||||
def run_settings_check(self, key, setting):
|
||||
"""Test that all settings are valid.
|
||||
|
||||
- Ensure that a name is set and that it is translated
|
||||
- Ensure that a description is set
|
||||
- Ensure that every setting key is valid
|
||||
- Ensure that a validator is supplied
|
||||
"""
|
||||
self.assertTrue(type(setting) is dict)
|
||||
|
||||
name = setting.get('name', None)
|
||||
@ -147,11 +148,11 @@ class SettingsTest(InvenTreeTestCase):
|
||||
self.assertIn(default, [True, False])
|
||||
|
||||
def test_setting_data(self):
|
||||
"""
|
||||
"""Test for settings data.
|
||||
|
||||
- Ensure that every setting has a name, which is translated
|
||||
- Ensure that every setting has a description, which is translated
|
||||
"""
|
||||
|
||||
for key, setting in InvenTreeSetting.SETTINGS.items():
|
||||
|
||||
try:
|
||||
@ -168,10 +169,7 @@ class SettingsTest(InvenTreeTestCase):
|
||||
raise exc
|
||||
|
||||
def test_defaults(self):
|
||||
"""
|
||||
Populate the settings with default values
|
||||
"""
|
||||
|
||||
"""Populate the settings with default values."""
|
||||
for key in InvenTreeSetting.SETTINGS.keys():
|
||||
|
||||
value = InvenTreeSetting.get_setting_default(key)
|
||||
@ -192,14 +190,10 @@ class SettingsTest(InvenTreeTestCase):
|
||||
|
||||
|
||||
class GlobalSettingsApiTest(InvenTreeAPITestCase):
|
||||
"""
|
||||
Tests for the global settings API
|
||||
"""
|
||||
"""Tests for the global settings API."""
|
||||
|
||||
def test_global_settings_api_list(self):
|
||||
"""
|
||||
Test list URL for global settings
|
||||
"""
|
||||
"""Test list URL for global settings."""
|
||||
url = reverse('api-global-setting-list')
|
||||
|
||||
# Read out each of the global settings value, to ensure they are instantiated in the database
|
||||
@ -212,7 +206,7 @@ class GlobalSettingsApiTest(InvenTreeAPITestCase):
|
||||
self.assertEqual(len(response.data), len(InvenTreeSetting.SETTINGS.keys()))
|
||||
|
||||
def test_company_name(self):
|
||||
|
||||
"""Test a settings object lifecyle e2e."""
|
||||
setting = InvenTreeSetting.get_setting_object('INVENTREE_COMPANY_NAME')
|
||||
|
||||
# Check default value
|
||||
@ -245,8 +239,7 @@ class GlobalSettingsApiTest(InvenTreeAPITestCase):
|
||||
self.assertEqual(setting.value, val)
|
||||
|
||||
def test_api_detail(self):
|
||||
"""Test that we can access the detail view for a setting based on the <key>"""
|
||||
|
||||
"""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"]:
|
||||
response = self.get(
|
||||
@ -287,28 +280,22 @@ class GlobalSettingsApiTest(InvenTreeAPITestCase):
|
||||
|
||||
|
||||
class UserSettingsApiTest(InvenTreeAPITestCase):
|
||||
"""
|
||||
Tests for the user settings API
|
||||
"""
|
||||
"""Tests for the user settings API."""
|
||||
|
||||
def test_user_settings_api_list(self):
|
||||
"""
|
||||
Test list URL for user settings
|
||||
"""
|
||||
"""Test list URL for user settings."""
|
||||
url = reverse('api-user-setting-list')
|
||||
|
||||
self.get(url, expected_code=200)
|
||||
|
||||
def test_user_setting_invalid(self):
|
||||
"""Test a user setting with an invalid key"""
|
||||
|
||||
"""Test a user setting with an invalid key."""
|
||||
url = reverse('api-user-setting-detail', kwargs={'key': 'DONKEY'})
|
||||
|
||||
self.get(url, expected_code=404)
|
||||
|
||||
def test_user_setting_init(self):
|
||||
"""Test we can retrieve a setting which has not yet been initialized"""
|
||||
|
||||
"""Test we can retrieve a setting which has not yet been initialized."""
|
||||
key = 'HOMEPAGE_PART_LATEST'
|
||||
|
||||
# Ensure it does not actually exist in the database
|
||||
@ -328,10 +315,7 @@ class UserSettingsApiTest(InvenTreeAPITestCase):
|
||||
self.assertEqual(setting.to_native_value(), False)
|
||||
|
||||
def test_user_setting_boolean(self):
|
||||
"""
|
||||
Test a boolean user setting value
|
||||
"""
|
||||
|
||||
"""Test a boolean user setting value."""
|
||||
# Ensure we have a boolean setting available
|
||||
setting = InvenTreeUserSetting.get_setting_object(
|
||||
'SEARCH_PREVIEW_SHOW_PARTS',
|
||||
@ -395,7 +379,7 @@ class UserSettingsApiTest(InvenTreeAPITestCase):
|
||||
self.assertFalse(str2bool(response.data['value']))
|
||||
|
||||
def test_user_setting_choice(self):
|
||||
|
||||
"""Test a user setting with choices."""
|
||||
setting = InvenTreeUserSetting.get_setting_object(
|
||||
'DATE_DISPLAY_FORMAT',
|
||||
user=self.user
|
||||
@ -434,7 +418,7 @@ class UserSettingsApiTest(InvenTreeAPITestCase):
|
||||
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
|
||||
@ -480,25 +464,25 @@ class UserSettingsApiTest(InvenTreeAPITestCase):
|
||||
|
||||
|
||||
class NotificationUserSettingsApiTest(InvenTreeAPITestCase):
|
||||
"""Tests for the notification user settings API"""
|
||||
"""Tests for the notification user settings API."""
|
||||
|
||||
def test_api_list(self):
|
||||
"""Test list URL"""
|
||||
"""Test list URL."""
|
||||
url = reverse('api-notifcation-setting-list')
|
||||
|
||||
self.get(url, expected_code=200)
|
||||
|
||||
def test_setting(self):
|
||||
"""Test the string name for NotificationUserSetting"""
|
||||
"""Test the string name for NotificationUserSetting."""
|
||||
test_setting = NotificationUserSetting.get_setting_object('NOTIFICATION_METHOD_MAIL', user=self.user)
|
||||
self.assertEqual(str(test_setting), 'NOTIFICATION_METHOD_MAIL (for testuser): ')
|
||||
|
||||
|
||||
class PluginSettingsApiTest(InvenTreeAPITestCase):
|
||||
"""Tests for the plugin settings API"""
|
||||
"""Tests for the plugin settings API."""
|
||||
|
||||
def test_plugin_list(self):
|
||||
"""List installed plugins via API"""
|
||||
"""List installed plugins via API."""
|
||||
url = reverse('api-plugin-list')
|
||||
|
||||
# Simple request
|
||||
@ -508,13 +492,13 @@ class PluginSettingsApiTest(InvenTreeAPITestCase):
|
||||
self.get(url, expected_code=200, data={'mixin': 'settings'})
|
||||
|
||||
def test_api_list(self):
|
||||
"""Test list URL"""
|
||||
"""Test list URL."""
|
||||
url = reverse('api-plugin-setting-list')
|
||||
|
||||
self.get(url, expected_code=200)
|
||||
|
||||
def test_valid_plugin_slug(self):
|
||||
"""Test that an valid plugin slug runs through"""
|
||||
"""Test that an valid plugin slug runs through."""
|
||||
# load plugin configs
|
||||
fixtures = PluginConfig.objects.all()
|
||||
if not fixtures:
|
||||
@ -544,26 +528,30 @@ class PluginSettingsApiTest(InvenTreeAPITestCase):
|
||||
self.assertIn("Plugin 'sample' has no setting matching 'doesnotexsist'", str(response.data))
|
||||
|
||||
def test_invalid_setting_key(self):
|
||||
"""Test that an invalid setting key returns a 404"""
|
||||
"""Test that an invalid setting key returns a 404."""
|
||||
...
|
||||
|
||||
def test_uninitialized_setting(self):
|
||||
"""Test that requesting an uninitialized setting creates the setting"""
|
||||
"""Test that requesting an uninitialized setting creates the setting."""
|
||||
...
|
||||
|
||||
|
||||
class WebhookMessageTests(TestCase):
|
||||
"""Tests for webhooks."""
|
||||
|
||||
def setUp(self):
|
||||
"""Setup for all tests."""
|
||||
self.endpoint_def = WebhookEndpoint.objects.create()
|
||||
self.url = f'/api/webhook/{self.endpoint_def.endpoint_id}/'
|
||||
self.client = Client(enforce_csrf_checks=True)
|
||||
|
||||
def test_bad_method(self):
|
||||
"""Test that a wrong HTTP method does not work."""
|
||||
response = self.client.get(self.url)
|
||||
|
||||
assert response.status_code == HTTPStatus.METHOD_NOT_ALLOWED
|
||||
|
||||
def test_missing_token(self):
|
||||
"""Tests that token checks work."""
|
||||
response = self.client.post(
|
||||
self.url,
|
||||
content_type=CONTENT_TYPE_JSON,
|
||||
@ -575,6 +563,7 @@ class WebhookMessageTests(TestCase):
|
||||
)
|
||||
|
||||
def test_bad_token(self):
|
||||
"""Test that a wrong token is not working."""
|
||||
response = self.client.post(
|
||||
self.url,
|
||||
content_type=CONTENT_TYPE_JSON,
|
||||
@ -585,6 +574,7 @@ class WebhookMessageTests(TestCase):
|
||||
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,
|
||||
@ -593,6 +583,7 @@ class WebhookMessageTests(TestCase):
|
||||
assert response.status_code == HTTPStatus.NOT_FOUND
|
||||
|
||||
def test_bad_json(self):
|
||||
"""Test that malformed JSON is not accepted."""
|
||||
response = self.client.post(
|
||||
self.url,
|
||||
data="{'this': 123}",
|
||||
@ -606,6 +597,7 @@ class WebhookMessageTests(TestCase):
|
||||
)
|
||||
|
||||
def test_success_no_token_check(self):
|
||||
"""Test that a endpoint without a token set does not require one."""
|
||||
# delete token
|
||||
self.endpoint_def.token = ''
|
||||
self.endpoint_def.save()
|
||||
@ -620,6 +612,7 @@ class WebhookMessageTests(TestCase):
|
||||
assert str(response.content, 'utf-8') == WebhookView.model_class.MESSAGE_OK
|
||||
|
||||
def test_bad_hmac(self):
|
||||
"""Test that a malformed HMAC does not pass."""
|
||||
# delete token
|
||||
self.endpoint_def.token = ''
|
||||
self.endpoint_def.secret = '123abc'
|
||||
@ -635,6 +628,7 @@ class WebhookMessageTests(TestCase):
|
||||
assert (json.loads(response.content)['detail'] == WebhookView.model_class.MESSAGE_TOKEN_ERROR)
|
||||
|
||||
def test_success_hmac(self):
|
||||
"""Test with a valid HMAC provided."""
|
||||
# delete token
|
||||
self.endpoint_def.token = ''
|
||||
self.endpoint_def.secret = '123abc'
|
||||
@ -651,6 +645,10 @@ class WebhookMessageTests(TestCase):
|
||||
assert str(response.content, 'utf-8') == WebhookView.model_class.MESSAGE_OK
|
||||
|
||||
def test_success(self):
|
||||
"""Test full e2e webhook call.
|
||||
|
||||
The message should go through and save the json payload.
|
||||
"""
|
||||
response = self.client.post(
|
||||
self.url,
|
||||
data={"this": "is a message"},
|
||||
@ -665,9 +663,10 @@ class WebhookMessageTests(TestCase):
|
||||
|
||||
|
||||
class NotificationTest(InvenTreeAPITestCase):
|
||||
"""Tests for NotificationEntriy."""
|
||||
|
||||
def test_check_notification_entries(self):
|
||||
|
||||
"""Test that notification entries can be created."""
|
||||
# Create some notification entries
|
||||
|
||||
self.assertEqual(NotificationEntry.objects.count(), 0)
|
||||
@ -684,21 +683,16 @@ class NotificationTest(InvenTreeAPITestCase):
|
||||
self.assertTrue(NotificationEntry.check_recent('test.notification', 1, delta))
|
||||
|
||||
def test_api_list(self):
|
||||
"""Test list URL"""
|
||||
"""Test list URL."""
|
||||
url = reverse('api-notifications-list')
|
||||
self.get(url, expected_code=200)
|
||||
|
||||
|
||||
class LoadingTest(TestCase):
|
||||
"""
|
||||
Tests for the common config
|
||||
"""
|
||||
"""Tests for the common config."""
|
||||
|
||||
def test_restart_flag(self):
|
||||
"""
|
||||
Test that the restart flag is reset on start
|
||||
"""
|
||||
|
||||
"""Test that the restart flag is reset on start."""
|
||||
import common.models
|
||||
from plugin import registry
|
||||
|
||||
@ -713,10 +707,10 @@ class LoadingTest(TestCase):
|
||||
|
||||
|
||||
class ColorThemeTest(TestCase):
|
||||
"""Tests for ColorTheme"""
|
||||
"""Tests for ColorTheme."""
|
||||
|
||||
def test_choices(self):
|
||||
"""Test that default choices are returned"""
|
||||
"""Test that default choices are returned."""
|
||||
result = ColorTheme.get_color_themes_choices()
|
||||
|
||||
# skip
|
||||
@ -725,7 +719,7 @@ class ColorThemeTest(TestCase):
|
||||
self.assertIn(('default', 'Default'), result)
|
||||
|
||||
def test_valid_choice(self):
|
||||
"""Check that is_valid_choice works correctly"""
|
||||
"""Check that is_valid_choice works correctly."""
|
||||
result = ColorTheme.get_color_themes_choices()
|
||||
|
||||
# skip
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
URL lookup for common views
|
||||
"""
|
||||
"""URL lookup for common views."""
|
||||
|
||||
common_urls = [
|
||||
]
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
Django views for interacting with common models
|
||||
"""
|
||||
"""Django views for interacting with common models."""
|
||||
|
||||
import os
|
||||
|
||||
@ -18,10 +16,10 @@ from .files import FileManager
|
||||
|
||||
|
||||
class MultiStepFormView(SessionWizardView):
|
||||
""" Setup basic methods of multi-step form
|
||||
"""Setup basic methods of multi-step form.
|
||||
|
||||
form_list: list of forms
|
||||
form_steps_description: description for each form
|
||||
form_list: list of forms
|
||||
form_steps_description: description for each form
|
||||
"""
|
||||
|
||||
form_steps_template = []
|
||||
@ -31,14 +29,13 @@ class MultiStepFormView(SessionWizardView):
|
||||
file_storage = FileSystemStorage(settings.MEDIA_ROOT)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
""" Override init method to set media folder """
|
||||
"""Override init method to set media folder."""
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.process_media_folder()
|
||||
|
||||
def process_media_folder(self):
|
||||
""" Process media folder """
|
||||
|
||||
"""Process media folder."""
|
||||
if self.media_folder:
|
||||
media_folder_abs = os.path.join(settings.MEDIA_ROOT, self.media_folder)
|
||||
if not os.path.exists(media_folder_abs):
|
||||
@ -46,8 +43,7 @@ class MultiStepFormView(SessionWizardView):
|
||||
self.file_storage = FileSystemStorage(location=media_folder_abs)
|
||||
|
||||
def get_template_names(self):
|
||||
""" Select template """
|
||||
|
||||
"""Select template."""
|
||||
try:
|
||||
# Get template
|
||||
template = self.form_steps_template[self.steps.index]
|
||||
@ -57,8 +53,7 @@ class MultiStepFormView(SessionWizardView):
|
||||
return template
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
""" Update context data """
|
||||
|
||||
"""Update context data."""
|
||||
# Retrieve current context
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
@ -74,7 +69,9 @@ class MultiStepFormView(SessionWizardView):
|
||||
|
||||
|
||||
class FileManagementFormView(MultiStepFormView):
|
||||
""" Setup form wizard to perform the following steps:
|
||||
"""File management form wizard.
|
||||
|
||||
Perform the following steps:
|
||||
1. Upload tabular data file
|
||||
2. Match headers to InvenTree fields
|
||||
3. Edit row data and match InvenTree items
|
||||
@ -95,8 +92,7 @@ class FileManagementFormView(MultiStepFormView):
|
||||
extra_context_data = {}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
""" Initialize the FormView """
|
||||
|
||||
"""Initialize the FormView."""
|
||||
# Perform all checks and inits for MultiStepFormView
|
||||
super().__init__(self, *args, **kwargs)
|
||||
|
||||
@ -105,8 +101,7 @@ class FileManagementFormView(MultiStepFormView):
|
||||
raise NotImplementedError('A subclass of a file manager class needs to be set!')
|
||||
|
||||
def get_context_data(self, form=None, **kwargs):
|
||||
""" Handle context data """
|
||||
|
||||
"""Handle context data."""
|
||||
if form is None:
|
||||
form = self.get_form()
|
||||
|
||||
@ -136,8 +131,7 @@ class FileManagementFormView(MultiStepFormView):
|
||||
return context
|
||||
|
||||
def get_file_manager(self, step=None, form=None):
|
||||
""" Get FileManager instance from uploaded file """
|
||||
|
||||
"""Get FileManager instance from uploaded file."""
|
||||
if self.file_manager:
|
||||
return
|
||||
|
||||
@ -151,8 +145,7 @@ class FileManagementFormView(MultiStepFormView):
|
||||
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 """
|
||||
|
||||
"""Update kwargs to dynamically build forms."""
|
||||
# Always retrieve FileManager instance from uploaded file
|
||||
self.get_file_manager(step)
|
||||
|
||||
@ -191,7 +184,7 @@ class FileManagementFormView(MultiStepFormView):
|
||||
return super().get_form_kwargs()
|
||||
|
||||
def get_form(self, step=None, data=None, files=None):
|
||||
""" add crispy-form helper to form """
|
||||
"""Add crispy-form helper to form."""
|
||||
form = super().get_form(step=step, data=data, files=files)
|
||||
|
||||
form.helper = FormHelper()
|
||||
@ -200,17 +193,14 @@ class FileManagementFormView(MultiStepFormView):
|
||||
return form
|
||||
|
||||
def get_form_table_data(self, form_data):
|
||||
""" Extract table cell data from form data and fields.
|
||||
These data are used to maintain state between sessions.
|
||||
"""Extract table cell data from form data and fields. These data are used to maintain state between sessions.
|
||||
|
||||
Table data keys are as follows:
|
||||
|
||||
col_name_<idx> - Column name at idx as provided in the uploaded file
|
||||
col_guess_<idx> - Column guess at idx as selected
|
||||
row_<x>_col<y> - Cell data as provided in the uploaded file
|
||||
|
||||
"""
|
||||
|
||||
# Map the columns
|
||||
self.column_names = {}
|
||||
self.column_selections = {}
|
||||
@ -264,8 +254,7 @@ class FileManagementFormView(MultiStepFormView):
|
||||
self.row_data[row_id][col_id] = value
|
||||
|
||||
def set_form_table_data(self, form=None):
|
||||
""" Set the form table data """
|
||||
|
||||
"""Set the form table data."""
|
||||
if self.column_names:
|
||||
# Re-construct the column data
|
||||
self.columns = []
|
||||
@ -324,10 +313,10 @@ class FileManagementFormView(MultiStepFormView):
|
||||
row[field_key] = field_key + '-' + str(row['index'])
|
||||
|
||||
def get_column_index(self, name):
|
||||
""" Return the index of the column with the given name.
|
||||
"""Return the index of the column with the given name.
|
||||
|
||||
It named column is not found, return -1
|
||||
"""
|
||||
|
||||
try:
|
||||
idx = list(self.column_selections.values()).index(name)
|
||||
except ValueError:
|
||||
@ -336,9 +325,7 @@ class FileManagementFormView(MultiStepFormView):
|
||||
return idx
|
||||
|
||||
def get_field_selection(self):
|
||||
""" Once data columns have been selected, attempt to pre-select the proper data from the database.
|
||||
This function is called once the field selection has been validated.
|
||||
The pre-fill data are then passed through to the part selection form.
|
||||
"""Once data columns have been selected, attempt to pre-select the proper data from the database. This function is called once the field selection has been validated. The pre-fill data are then passed through to the part selection form.
|
||||
|
||||
This method is very specific to the type of data found in the file,
|
||||
therefore overwrite it in the subclass.
|
||||
@ -346,7 +333,7 @@ class FileManagementFormView(MultiStepFormView):
|
||||
pass
|
||||
|
||||
def get_clean_items(self):
|
||||
""" returns dict with all cleaned values """
|
||||
"""Returns dict with all cleaned values."""
|
||||
items = {}
|
||||
|
||||
for form_key, form_value in self.get_all_cleaned_data().items():
|
||||
@ -373,8 +360,7 @@ class FileManagementFormView(MultiStepFormView):
|
||||
return items
|
||||
|
||||
def check_field_selection(self, form):
|
||||
""" Check field matching """
|
||||
|
||||
"""Check field matching."""
|
||||
# Are there any missing columns?
|
||||
missing_columns = []
|
||||
|
||||
@ -422,8 +408,7 @@ class FileManagementFormView(MultiStepFormView):
|
||||
return valid
|
||||
|
||||
def validate(self, step, form):
|
||||
""" Validate forms """
|
||||
|
||||
"""Validate forms."""
|
||||
valid = True
|
||||
|
||||
# Get form table data
|
||||
@ -442,8 +427,7 @@ class FileManagementFormView(MultiStepFormView):
|
||||
return valid
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
""" Perform validations before posting data """
|
||||
|
||||
"""Perform validations before posting data."""
|
||||
wizard_goto_step = self.request.POST.get('wizard_goto_step', None)
|
||||
|
||||
form = self.get_form(data=self.request.POST, files=self.request.FILES)
|
||||
@ -458,14 +442,21 @@ class FileManagementFormView(MultiStepFormView):
|
||||
|
||||
|
||||
class FileManagementAjaxView(AjaxView):
|
||||
""" Use a FileManagementFormView as base for a AjaxView
|
||||
Inherit this class before inheriting the base FileManagementFormView
|
||||
"""Use a FileManagementFormView as base for a AjaxView Inherit this class before inheriting the base FileManagementFormView.
|
||||
|
||||
ajax_form_steps_template: templates for rendering ajax
|
||||
validate: function to validate the current form -> normally point to the same function in the base FileManagementFormView
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
"""Handle wizard step call.
|
||||
|
||||
Possible actions:
|
||||
- Step back -> render previous step
|
||||
- Invalid form -> render error
|
||||
- Valid form and not done -> render next step
|
||||
- Valid form and done -> render final step
|
||||
"""
|
||||
# check if back-step button was selected
|
||||
wizard_back = self.request.POST.get('act-btn_back', None)
|
||||
if wizard_back:
|
||||
@ -497,6 +488,7 @@ class FileManagementAjaxView(AjaxView):
|
||||
return self.renderJsonResponse(request, data={'form_valid': None})
|
||||
|
||||
def get(self, request):
|
||||
"""Reset storage if flag is set, proceed to render JsonResponse."""
|
||||
if 'reset' in request.GET:
|
||||
# reset form
|
||||
self.storage.reset()
|
||||
@ -504,11 +496,12 @@ class FileManagementAjaxView(AjaxView):
|
||||
return self.renderJsonResponse(request)
|
||||
|
||||
def renderJsonResponse(self, request, form=None, data={}, context=None):
|
||||
""" always set the right templates before rendering """
|
||||
"""Always set the right templates before rendering."""
|
||||
self.setTemplate()
|
||||
return super().renderJsonResponse(request, form=form, data=data, context=context)
|
||||
|
||||
def get_data(self):
|
||||
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 []
|
||||
@ -516,9 +509,13 @@ class FileManagementAjaxView(AjaxView):
|
||||
return data
|
||||
|
||||
def setTemplate(self):
|
||||
""" set template name and title """
|
||||
"""Set template name and title."""
|
||||
self.ajax_template_name = self.ajax_form_steps_template[self.get_step_index()]
|
||||
self.ajax_form_title = self.form_steps_description[self.get_step_index()]
|
||||
|
||||
def validate(self, obj, form, **kwargs):
|
||||
"""Generic validate action.
|
||||
|
||||
This is the point to process provided userinput.
|
||||
"""
|
||||
raise NotImplementedError('This function needs to be overridden!')
|
||||
|
Reference in New Issue
Block a user