diff --git a/cookbook/admin.py b/cookbook/admin.py index fbfdaaa2b..cf0470ac9 100644 --- a/cookbook/admin.py +++ b/cookbook/admin.py @@ -91,8 +91,8 @@ admin.site.register(SearchPreference, SearchPreferenceAdmin) class AiProviderAdmin(admin.ModelAdmin): - list_display = ('name', 'space', 'model',) - search_fields = ('name', 'space', 'model',) + list_display = ('name', 'space', 'model_name',) + search_fields = ('name', 'space', 'model_name',) admin.site.register(AiProvider, AiProviderAdmin) diff --git a/cookbook/helper/ai_helper.py b/cookbook/helper/ai_helper.py index 71668e2fd..4c5c19b13 100644 --- a/cookbook/helper/ai_helper.py +++ b/cookbook/helper/ai_helper.py @@ -62,7 +62,7 @@ class AiCallbackHandler(CustomLogger): remaining_balance = self.space.ai_credits_balance - Decimal(str(credit_cost)) if remaining_balance < 0: remaining_balance = 0 - if settings.HOSTED: + if settings.HOSTED and self.space.ai_credits_monthly == 0: self.space.ai_enabled = False self.space.ai_credits_balance = remaining_balance diff --git a/cookbook/models.py b/cookbook/models.py index a6cbb2a75..3a287ef8c 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -417,6 +417,9 @@ class AiProvider(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + def __str__(self): + return self.name + class AiLog(models.Model, PermissionModelMixin): F_FILE_IMPORT = 'FILE_IMPORT' @@ -437,6 +440,9 @@ class AiLog(models.Model, PermissionModelMixin): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + def __str__(self): + return f"{self.function} {self.ai_provider.name} {self.created_at}" + class ConnectorConfig(models.Model, PermissionModelMixin): HOMEASSISTANT = 'HomeAssistant' diff --git a/cookbook/serializer.py b/cookbook/serializer.py index dfdf035a2..7f76a9070 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -335,17 +335,23 @@ class AiProviderSerializer(serializers.ModelSerializer): return super().create(validated_data) def update(self, instance, validated_data): - validated_data = self.handle_global_space_logic(validated_data) + validated_data = self.handle_global_space_logic(validated_data, instance=instance) return super().update(instance, validated_data) - def handle_global_space_logic(self, validated_data): + def handle_global_space_logic(self, validated_data, instance=None): """ allow superusers to create AI providers without a space but make sure everyone else only uses their own space """ if ('space' not in validated_data or not validated_data['space']) and self.context['request'].user.is_superuser: validated_data['space'] = None else: - validated_data['space'] = self.context['request'].space + if instance: + validated_data['space'] = instance.space + else: + validated_data['space'] = self.context['request'].space + + if 'log_credit_cost' in validated_data and not self.context['request'].user.is_superuser: + del validated_data['log_credit_cost'] return validated_data @@ -1709,6 +1715,11 @@ class FdcQuerySerializer(serializers.Serializer): foods = FdcQueryFoodsSerializer(many=True) +class GenericModelSerializer(serializers.Serializer): + id = serializers.IntegerField() + model = serializers.CharField() + name = serializers.CharField() + # Export/Import Serializers class KeywordExportSerializer(KeywordSerializer): diff --git a/cookbook/views/api.py b/cookbook/views/api.py index f119fdc7b..66015d665 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -19,12 +19,15 @@ import redis import requests from PIL import UnidentifiedImageError from django.contrib import messages +from django.contrib.admin.utils import get_deleted_objects, NestedObjects from django.contrib.auth.models import Group, User from django.contrib.postgres.search import TrigramSimilarity from django.core.cache import caches from django.core.exceptions import FieldError, ValidationError from django.core.files import File +from django.db import DEFAULT_DB_ALIAS from django.db.models import Case, Count, Exists, OuterRef, ProtectedError, Q, Subquery, Value, When +from django.db.models.deletion import Collector from django.db.models.fields.related import ForeignObjectRel from django.db.models.functions import Coalesce, Lower from django.db.models.signals import post_save @@ -110,7 +113,7 @@ from cookbook.serializer import (AccessTokenSerializer, AutomationSerializer, Au LocalizationSerializer, ServerSettingsSerializer, RecipeFromSourceResponseSerializer, ShoppingListEntryBulkCreateSerializer, FdcQuerySerializer, AiImportSerializer, ImportOpenDataSerializer, ImportOpenDataMetaDataSerializer, ImportOpenDataResponseSerializer, ExportRequestSerializer, RecipeImportSerializer, ConnectorConfigSerializer, SearchPreferenceSerializer, SearchFieldsSerializer, RecipeBatchUpdateSerializer, - AiProviderSerializer, AiLogSerializer, FoodBatchUpdateSerializer + AiProviderSerializer, AiLogSerializer, FoodBatchUpdateSerializer, GenericModelSerializer ) from cookbook.version_info import TANDOOR_VERSION from cookbook.views.import_export import get_integration @@ -1648,6 +1651,27 @@ class PropertyTypeViewSet(LoggingMixin, viewsets.ModelViewSet): self.queryset.filter(category__in=category) return self.queryset.filter(space=self.request.space) + @extend_schema(responses=GenericModelSerializer(many=True)) + @decorators.action(detail=True, methods=['GET'], serializer_class=GenericModelSerializer, pagination_class=DefaultPagination) + # TODO actually implement pagination + def protecting(self, request, pk): + obj = self.queryset.filter(pk=pk, space=request.space).first() + if obj: + collector = NestedObjects(using=DEFAULT_DB_ALIAS) + collector.collect([obj]) + + protected_objects = [] + for o in collector.protected: + protected_objects.append({ + 'id': o.pk, + 'model': o.__class__.__name__, + 'name': str(o), + }) + + return Response(self.serializer_class(protected_objects, many=True, context={'request': request}).data) + else: + return Response({}, status=status.HTTP_404_NOT_FOUND) + class PropertyViewSet(LoggingMixin, viewsets.ModelViewSet): queryset = Property.objects