diff --git a/cookbook/helper/batch_edit_helper.py b/cookbook/helper/batch_edit_helper.py new file mode 100644 index 000000000..3d2daa673 --- /dev/null +++ b/cookbook/helper/batch_edit_helper.py @@ -0,0 +1,22 @@ +def add_to_relation(relation_model, base_field_name, base_ids, related_field_name, related_ids): + """ + given a model, the base and related field and the base and related ids, bulk create relation objects + """ + relation_objects = [] + for b in base_ids: + for r in related_ids: + relation_objects.append(relation_model(**{base_field_name: b, related_field_name: r})) + relation_model.objects.bulk_create(relation_objects, ignore_conflicts=True, unique_fields=(base_field_name, related_field_name,)) + + +def remove_from_relation(relation_model, base_field_name, base_ids, related_field_name, related_ids): + relation_model.objects.filter(**{f'{base_field_name}__in': base_ids, f'{related_field_name}__in': related_ids}).delete() + + +def remove_all_from_relation(relation_model, base_field_name, base_ids): + relation_model.objects.filter(**{f'{base_field_name}__in': base_ids}).delete() + + +def set_relation(relation_model, base_field_name, base_ids, related_field_name, related_ids): + remove_all_from_relation(relation_model, base_field_name, base_ids) + add_to_relation(relation_model, base_field_name, base_ids, related_field_name, related_ids) diff --git a/cookbook/serializer.py b/cookbook/serializer.py index 322dcd35c..07fccce24 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -1218,6 +1218,9 @@ class FoodBatchUpdateSerializer(serializers.Serializer): ignore_shopping = serializers.BooleanField(required=False, allow_null=True) on_hand = serializers.BooleanField(required=False, allow_null=True) + parent_remove = serializers.BooleanField(required=False, allow_null=True) + parent_set = serializers.IntegerField(required=False, allow_null=True) + class CustomFilterSerializer(SpacedModelSerializer, WritableNestedModelSerializer): shared = UserSerializer(many=True, required=False) diff --git a/cookbook/views/api.py b/cookbook/views/api.py index a71aea796..7d503041b 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -66,6 +66,7 @@ from cookbook.forms import ImportForm, ImportExportBase from cookbook.helper import recipe_url_import as helper from cookbook.helper.HelperFunctions import str2bool, validate_import_url from cookbook.helper.ai_helper import has_monthly_token, can_perform_ai_request, AiCallbackHandler +from cookbook.helper.batch_edit_helper import add_to_relation, remove_from_relation, remove_all_from_relation, set_relation from cookbook.helper.image_processing import handle_image from cookbook.helper.ingredient_parser import IngredientParser from cookbook.helper.open_data_importer import OpenDataImporter @@ -951,7 +952,6 @@ class FoodViewSet(LoggingMixin, TreeMixin): if 'category' in serializer.validated_data: foods.update(supermarket_category_id=serializer.validated_data['category']) - if 'ignore_shopping' in serializer.validated_data and serializer.validated_data['ignore_shopping'] is not None: foods.update(ignore_shopping=serializer.validated_data['ignore_shopping']) @@ -964,32 +964,66 @@ class FoodViewSet(LoggingMixin, TreeMixin): else: Food.onhand_users.through.objects.filter(food_id__in=safe_food_ids, user_id=request.user.id).delete() - if 'substitutes_add' in serializer.validated_data: - substitute_relation = [] - for f in safe_food_ids: - for s in serializer.validated_data['substitutes_add']: - substitute_relation.append(Food.substitute.through(from_food_id=f, to_food_id=s)) - Food.substitute.through.objects.bulk_create(substitute_relation, ignore_conflicts=True, unique_fields=('from_food_id', 'to_food_id',)) + if 'substitute_children' in serializer.validated_data and serializer.validated_data['substitute_children'] is not None: + foods.update(substitute_children=serializer.validated_data['substitute_children']) + + if 'substitute_siblings' in serializer.validated_data and serializer.validated_data['substitute_siblings'] is not None: + foods.update(substitute_siblings=serializer.validated_data['substitute_siblings']) + + # ---------- substitutes ------------- + if 'substitute_add' in serializer.validated_data: + add_to_relation(Food.substitute.through, 'from_food_id', safe_food_ids, 'to_food_id', serializer.validated_data['substitute_add']) if 'substitute_remove' in serializer.validated_data: - for s in serializer.validated_data['substitute_remove']: - Food.substitute.through.objects.filter(from_food_id=safe_food_ids, to_food_id=s).delete() + remove_from_relation(Food.substitute.through, 'from_food_id', safe_food_ids, 'to_food_id', serializer.validated_data['substitute_remove']) if 'substitute_set' in serializer.validated_data and len(serializer.validated_data['substitute_set']) > 0: - substitute_relation = [] - Food.substitute.through.objects.filter(from_food_id=safe_food_ids).delete() - for f in safe_food_ids: - for s in serializer.validated_data['substitute_set']: - substitute_relation.append(Food.substitute.through(from_food_id=f, to_food_id=s)) - Food.substitute.through.objects.bulk_create(substitute_relation, ignore_conflicts=True, unique_fields=('from_food_id', 'to_food_id',)) + set_relation(Food.substitute.through, 'from_food_id', safe_food_ids, 'to_food_id', serializer.validated_data['substitute_set']) if 'substitute_remove_all' in serializer.validated_data and serializer.validated_data['substitute_remove_all']: - Food.substitute.through.objects.filter(from_food_id=safe_food_ids).delete() + remove_all_from_relation(Food.substitute.through, 'from_food_id', safe_food_ids) + # ---------- inherit fields ------------- + if 'inherit_fields_add' in serializer.validated_data: + add_to_relation(Food.inherit_fields.through, 'food_id', safe_food_ids, 'foodinheritfield_id', serializer.validated_data['inherit_fields_add']) - def add_substitute(relation_model, base_field, base_ids, related_field_name, related_ids ): - pass + if 'inherit_fields_remove' in serializer.validated_data: + remove_from_relation(Food.inherit_fields.through, 'food_id', safe_food_ids, 'foodinheritfield_id', serializer.validated_data['inherit_fields_remove']) + if 'inherit_fields_set' in serializer.validated_data and len(serializer.validated_data['inherit_fields_set']) > 0: + set_relation(Food.inherit_fields.through, 'food_id', safe_food_ids, 'foodinheritfield_id', serializer.validated_data['inherit_fields_set']) + + if 'inherit_fields_remove_all' in serializer.validated_data and serializer.validated_data['inherit_fields_remove_all']: + remove_all_from_relation(Food.inherit_fields.through, 'food_id', safe_food_ids) + + # ---------- child inherit fields ------------- + if 'child_inherit_fields_add' in serializer.validated_data: + add_to_relation(Food.child_inherit_fields.through, 'food_id', safe_food_ids, 'foodinheritfield_id', serializer.validated_data['child_inherit_fields_add']) + + if 'child_inherit_fields_remove' in serializer.validated_data: + remove_from_relation(Food.child_inherit_fields.through, 'food_id', safe_food_ids, 'foodinheritfield_id', serializer.validated_data['child_inherit_fields_remove']) + + if 'child_inherit_fields_set' in serializer.validated_data and len(serializer.validated_data['child_inherit_fields_set']) > 0: + set_relation(Food.child_inherit_fields.through, 'food_id', safe_food_ids, 'foodinheritfield_id', serializer.validated_data['child_inherit_fields_set']) + + if 'child_inherit_fields_remove_all' in serializer.validated_data and serializer.validated_data['child_inherit_fields_remove_all']: + remove_all_from_relation(Food.child_inherit_fields.through, 'food_id', safe_food_ids) + + # ------- parent -------- + if self.model.node_order_by: + node_location = 'sorted' + else: + node_location = 'last' + + if 'parent_remove' in serializer.validated_data and serializer.validated_data['parent_remove']: + for f in foods: + f.move(Food.get_first_root_node(), f'{node_location}-sibling') + + if 'parent_set' in serializer.validated_data: + parent_food = Food.objects.filter(space=request.space, id=serializer.validated_data['parent_set']).first() + if parent_food: + for f in foods: + f.move(parent_food, f'{node_location}-child') return Response({}, 200) diff --git a/vue3/src/components/dialogs/BatchEditFoodDialog.vue b/vue3/src/components/dialogs/BatchEditFoodDialog.vue index 921de0a52..8de3f5bb7 100644 --- a/vue3/src/components/dialogs/BatchEditFoodDialog.vue +++ b/vue3/src/components/dialogs/BatchEditFoodDialog.vue @@ -42,6 +42,9 @@ + + + @@ -50,8 +53,13 @@ - - + + + + + diff --git a/vue3/src/openapi/models/FoodBatchUpdate.ts b/vue3/src/openapi/models/FoodBatchUpdate.ts index 932d212a6..1f9c88cef 100644 --- a/vue3/src/openapi/models/FoodBatchUpdate.ts +++ b/vue3/src/openapi/models/FoodBatchUpdate.ts @@ -127,6 +127,18 @@ export interface FoodBatchUpdate { * @memberof FoodBatchUpdate */ onHand?: boolean; + /** + * + * @type {boolean} + * @memberof FoodBatchUpdate + */ + parentRemove?: boolean; + /** + * + * @type {number} + * @memberof FoodBatchUpdate + */ + parentSet?: number; } /** @@ -174,6 +186,8 @@ export function FoodBatchUpdateFromJSONTyped(json: any, ignoreDiscriminator: boo 'substituteSiblings': json['substitute_siblings'] == null ? undefined : json['substitute_siblings'], 'ignoreShopping': json['ignore_shopping'] == null ? undefined : json['ignore_shopping'], 'onHand': json['on_hand'] == null ? undefined : json['on_hand'], + 'parentRemove': json['parent_remove'] == null ? undefined : json['parent_remove'], + 'parentSet': json['parent_set'] == null ? undefined : json['parent_set'], }; } @@ -201,6 +215,8 @@ export function FoodBatchUpdateToJSON(value?: FoodBatchUpdate | null): any { 'substitute_siblings': value['substituteSiblings'], 'ignore_shopping': value['ignoreShopping'], 'on_hand': value['onHand'], + 'parent_remove': value['parentRemove'], + 'parent_set': value['parentSet'], }; }