From ad32e457fa06cd3071ae6bc04b333a5d23547af9 Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Sat, 20 Sep 2025 12:05:29 +0200 Subject: [PATCH] basics of delete collector logic --- cookbook/views/api.py | 98 +++++++++++++---- vue3/src/apps/tandoor/main.ts | 1 + vue3/src/openapi/.openapi-generator/FILES | 2 + vue3/src/openapi/apis/ApiApi.ts | 54 ++++++++++ vue3/src/openapi/models/GenericModel.ts | 78 ++++++++++++++ .../models/PaginatedGenericModelList.ts | 101 ++++++++++++++++++ vue3/src/openapi/models/index.ts | 2 + vue3/src/pages/ModelDeletePage.vue | 68 ++++++++++++ 8 files changed, 382 insertions(+), 22 deletions(-) create mode 100644 vue3/src/openapi/models/GenericModel.ts create mode 100644 vue3/src/openapi/models/PaginatedGenericModelList.ts create mode 100644 vue3/src/pages/ModelDeletePage.vue diff --git a/cookbook/views/api.py b/cookbook/views/api.py index 66015d665..53ce3246c 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -9,6 +9,7 @@ import threading import traceback import uuid from collections import OrderedDict +from functools import wraps from json import JSONDecodeError from urllib.parse import unquote from zipfile import ZipFile @@ -26,7 +27,7 @@ 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 import Case, Count, Exists, OuterRef, ProtectedError, Q, Subquery, Value, When, QuerySet from django.db.models.deletion import Collector from django.db.models.fields.related import ForeignObjectRel from django.db.models.functions import Coalesce, Lower @@ -784,6 +785,22 @@ class KeywordViewSet(LoggingMixin, TreeMixin): pagination_class = DefaultPagination +def paginate(func): + + @wraps(func) + def inner(self, *args, **kwargs): + queryset = func(self, *args, **kwargs) + assert isinstance(queryset, (list, QuerySet)) + + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) + return inner + class UnitViewSet(LoggingMixin, MergeMixin, FuzzyFilterMixin): queryset = Unit.objects model = Unit @@ -791,6 +808,64 @@ class UnitViewSet(LoggingMixin, MergeMixin, FuzzyFilterMixin): permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope] pagination_class = DefaultPagination + @extend_schema(responses=GenericModelSerializer(many=True)) + @decorators.action(detail=True, methods=['GET'], serializer_class=GenericModelSerializer, pagination_class=DefaultPagination) + @paginate + 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]) + # TODO caching + # for units + # foodproperty_unit = PROTECT + # Ingredient = SET NULL + # UnitConversion = CASCADE + + #print(collector.nested()) # nested seems to not include protecting but does include cascading + + # protected: objects that raise Protected or Restricted error when deleting unit + # field_updates: fields that get updated when deleting the unit + # model objs: collects the objects that should be deleted together with the selected unit + + updating_objects = [] + # field_updates is a dict of relations that will be updated and querysets of items affected + for key, value in collector.field_updates.items(): + # iterate over each queryset for relation + for qs in value: + # itereate over each object in queryset of relation + for o in qs: + updating_objects.append( + { + 'id': o.pk, + 'model': o.__class__.__name__, + 'name': str(o), + } + ) + + cascading_objects = [] + print(collector.model_objs) + for model, objs in collector.model_objs.items(): + for o in objs: + cascading_objects.append({ + 'id': o.pk, + 'model': o.__class__.__name__, + 'name': str(o), + }) + + # django admin only shows protected and CASCADING, SET_NULL is not shown/warned of + + protected_objects = [] + for o in collector.protected: + protected_objects.append({ + 'id': o.pk, + 'model': o.__class__.__name__, + 'name': str(o), + }) + + return cascading_objects + else: + return [] class FoodInheritFieldViewSet(LoggingMixin, viewsets.ReadOnlyModelViewSet): queryset = FoodInheritField.objects @@ -1651,27 +1726,6 @@ 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 diff --git a/vue3/src/apps/tandoor/main.ts b/vue3/src/apps/tandoor/main.ts index 490400452..cc33c12a6 100644 --- a/vue3/src/apps/tandoor/main.ts +++ b/vue3/src/apps/tandoor/main.ts @@ -47,6 +47,7 @@ let routes = [ {path: '/list/:model?', component: () => import("@/pages/ModelListPage.vue"), props: true, name: 'ModelListPage'}, {path: '/edit/:model/:id?', component: () => import("@/pages/ModelEditPage.vue"), props: true, name: 'ModelEditPage'}, + {path: '/delete/:model/:id?', component: () => import("@/pages/ModelDeletePage.vue"), props: true, name: 'ModelDeletePage'}, {path: '/database', component: () => import("@/pages/DatabasePage.vue"), props: true, name: 'DatabasePage', meta: {title: 'Database'}}, {path: '/ingredient-editor', component: () => import("@/pages/IngredientEditorPage.vue"), name: 'IngredientEditorPage', meta: {title: 'Ingredient Editor'}}, diff --git a/vue3/src/openapi/.openapi-generator/FILES b/vue3/src/openapi/.openapi-generator/FILES index b24b8fbc7..8b6f5489a 100644 --- a/vue3/src/openapi/.openapi-generator/FILES +++ b/vue3/src/openapi/.openapi-generator/FILES @@ -33,6 +33,7 @@ models/FoodBatchUpdate.ts models/FoodInheritField.ts models/FoodShoppingUpdate.ts models/FoodSimple.ts +models/GenericModel.ts models/Group.ts models/ImportLog.ts models/ImportOpenData.ts @@ -72,6 +73,7 @@ models/PaginatedEnterpriseSocialRecipeSearchList.ts models/PaginatedEnterpriseSpaceList.ts models/PaginatedExportLogList.ts models/PaginatedFoodList.ts +models/PaginatedGenericModelList.ts models/PaginatedImportLogList.ts models/PaginatedIngredientList.ts models/PaginatedInviteLinkList.ts diff --git a/vue3/src/openapi/apis/ApiApi.ts b/vue3/src/openapi/apis/ApiApi.ts index ef7f089e4..30d0e60db 100644 --- a/vue3/src/openapi/apis/ApiApi.ts +++ b/vue3/src/openapi/apis/ApiApi.ts @@ -64,6 +64,7 @@ import type { PaginatedEnterpriseSpaceList, PaginatedExportLogList, PaginatedFoodList, + PaginatedGenericModelList, PaginatedImportLogList, PaginatedIngredientList, PaginatedInviteLinkList, @@ -281,6 +282,8 @@ import { PaginatedExportLogListToJSON, PaginatedFoodListFromJSON, PaginatedFoodListToJSON, + PaginatedGenericModelListFromJSON, + PaginatedGenericModelListToJSON, PaginatedImportLogListFromJSON, PaginatedImportLogListToJSON, PaginatedIngredientListFromJSON, @@ -2065,6 +2068,12 @@ export interface ApiUnitPartialUpdateRequest { patchedUnit?: PatchedUnit; } +export interface ApiUnitProtectingListRequest { + id: number; + page?: number; + pageSize?: number; +} + export interface ApiUnitRetrieveRequest { id: number; } @@ -15481,6 +15490,51 @@ export class ApiApi extends runtime.BaseAPI { return await response.value(); } + /** + * logs request counts to redis cache total/per user/ + */ + async apiUnitProtectingListRaw(requestParameters: ApiUnitProtectingListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters['id'] == null) { + throw new runtime.RequiredError( + 'id', + 'Required parameter "id" was null or undefined when calling apiUnitProtectingList().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['page'] != null) { + queryParameters['page'] = requestParameters['page']; + } + + if (requestParameters['pageSize'] != null) { + queryParameters['page_size'] = requestParameters['pageSize']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication + } + + const response = await this.request({ + path: `/api/unit/{id}/protecting/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))), + method: 'GET', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => PaginatedGenericModelListFromJSON(jsonValue)); + } + + /** + * logs request counts to redis cache total/per user/ + */ + async apiUnitProtectingList(requestParameters: ApiUnitProtectingListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.apiUnitProtectingListRaw(requestParameters, initOverrides); + return await response.value(); + } + /** * logs request counts to redis cache total/per user/ */ diff --git a/vue3/src/openapi/models/GenericModel.ts b/vue3/src/openapi/models/GenericModel.ts new file mode 100644 index 000000000..40ca10815 --- /dev/null +++ b/vue3/src/openapi/models/GenericModel.ts @@ -0,0 +1,78 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Tandoor + * Tandoor API Docs + * + * The version of the OpenAPI document: 0.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +/** + * + * @export + * @interface GenericModel + */ +export interface GenericModel { + /** + * + * @type {number} + * @memberof GenericModel + */ + id?: number; + /** + * + * @type {string} + * @memberof GenericModel + */ + model: string; + /** + * + * @type {string} + * @memberof GenericModel + */ + name: string; +} + +/** + * Check if a given object implements the GenericModel interface. + */ +export function instanceOfGenericModel(value: object): value is GenericModel { + if (!('model' in value) || value['model'] === undefined) return false; + if (!('name' in value) || value['name'] === undefined) return false; + return true; +} + +export function GenericModelFromJSON(json: any): GenericModel { + return GenericModelFromJSONTyped(json, false); +} + +export function GenericModelFromJSONTyped(json: any, ignoreDiscriminator: boolean): GenericModel { + if (json == null) { + return json; + } + return { + + 'id': json['id'] == null ? undefined : json['id'], + 'model': json['model'], + 'name': json['name'], + }; +} + +export function GenericModelToJSON(value?: GenericModel | null): any { + if (value == null) { + return value; + } + return { + + 'id': value['id'], + 'model': value['model'], + 'name': value['name'], + }; +} + diff --git a/vue3/src/openapi/models/PaginatedGenericModelList.ts b/vue3/src/openapi/models/PaginatedGenericModelList.ts new file mode 100644 index 000000000..6c4b500ea --- /dev/null +++ b/vue3/src/openapi/models/PaginatedGenericModelList.ts @@ -0,0 +1,101 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Tandoor + * Tandoor API Docs + * + * The version of the OpenAPI document: 0.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +import type { GenericModel } from './GenericModel'; +import { + GenericModelFromJSON, + GenericModelFromJSONTyped, + GenericModelToJSON, +} from './GenericModel'; + +/** + * + * @export + * @interface PaginatedGenericModelList + */ +export interface PaginatedGenericModelList { + /** + * + * @type {number} + * @memberof PaginatedGenericModelList + */ + count: number; + /** + * + * @type {string} + * @memberof PaginatedGenericModelList + */ + next?: string; + /** + * + * @type {string} + * @memberof PaginatedGenericModelList + */ + previous?: string; + /** + * + * @type {Array} + * @memberof PaginatedGenericModelList + */ + results: Array; + /** + * + * @type {Date} + * @memberof PaginatedGenericModelList + */ + timestamp?: Date; +} + +/** + * Check if a given object implements the PaginatedGenericModelList interface. + */ +export function instanceOfPaginatedGenericModelList(value: object): value is PaginatedGenericModelList { + if (!('count' in value) || value['count'] === undefined) return false; + if (!('results' in value) || value['results'] === undefined) return false; + return true; +} + +export function PaginatedGenericModelListFromJSON(json: any): PaginatedGenericModelList { + return PaginatedGenericModelListFromJSONTyped(json, false); +} + +export function PaginatedGenericModelListFromJSONTyped(json: any, ignoreDiscriminator: boolean): PaginatedGenericModelList { + if (json == null) { + return json; + } + return { + + 'count': json['count'], + 'next': json['next'] == null ? undefined : json['next'], + 'previous': json['previous'] == null ? undefined : json['previous'], + 'results': ((json['results'] as Array).map(GenericModelFromJSON)), + 'timestamp': json['timestamp'] == null ? undefined : (new Date(json['timestamp'])), + }; +} + +export function PaginatedGenericModelListToJSON(value?: PaginatedGenericModelList | null): any { + if (value == null) { + return value; + } + return { + + 'count': value['count'], + 'next': value['next'], + 'previous': value['previous'], + 'results': ((value['results'] as Array).map(GenericModelToJSON)), + 'timestamp': value['timestamp'] == null ? undefined : ((value['timestamp']).toISOString()), + }; +} + diff --git a/vue3/src/openapi/models/index.ts b/vue3/src/openapi/models/index.ts index dfd526d06..84ba030e6 100644 --- a/vue3/src/openapi/models/index.ts +++ b/vue3/src/openapi/models/index.ts @@ -31,6 +31,7 @@ export * from './FoodBatchUpdate'; export * from './FoodInheritField'; export * from './FoodShoppingUpdate'; export * from './FoodSimple'; +export * from './GenericModel'; export * from './Group'; export * from './ImportLog'; export * from './ImportOpenData'; @@ -70,6 +71,7 @@ export * from './PaginatedEnterpriseSocialRecipeSearchList'; export * from './PaginatedEnterpriseSpaceList'; export * from './PaginatedExportLogList'; export * from './PaginatedFoodList'; +export * from './PaginatedGenericModelList'; export * from './PaginatedImportLogList'; export * from './PaginatedIngredientList'; export * from './PaginatedInviteLinkList'; diff --git a/vue3/src/pages/ModelDeletePage.vue b/vue3/src/pages/ModelDeletePage.vue new file mode 100644 index 000000000..a2076dcc2 --- /dev/null +++ b/vue3/src/pages/ModelDeletePage.vue @@ -0,0 +1,68 @@ + + + + + \ No newline at end of file