diff --git a/cookbook/serializer.py b/cookbook/serializer.py index 3fffdea7..e5a4b9cc 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -155,7 +155,7 @@ class FoodInheritFieldSerializer(UniqueFieldsMixin): class UserPreferenceSerializer(serializers.ModelSerializer): # food_inherit_default = FoodInheritFieldSerializer(source='space.food_inherit', read_only=True) food_ignore_default = serializers.SerializerMethodField('get_ignore_default') - plan_share = UserNameSerializer(many=True) + plan_share = UserNameSerializer(many=True, allow_null=True, required=False) def get_ignore_default(self, obj): return FoodInheritFieldSerializer(Food.inherit_fields.difference(obj.space.food_inherit.all()), many=True).data @@ -604,7 +604,7 @@ class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer): meal_type_name = serializers.ReadOnlyField(source='meal_type.name') # TODO deprecate once old meal plan was removed note_markdown = serializers.SerializerMethodField('get_note_markdown') servings = CustomDecimalField() - shared = UserNameSerializer(many=True) + shared = UserNameSerializer(many=True, required=False, allow_null=True) def get_note_markdown(self, obj): return markdown(obj.note) diff --git a/cookbook/tests/api/test_api_shopping_list_entryv2.py b/cookbook/tests/api/test_api_shopping_list_entryv2.py index 3599dcfe..84ee22ba 100644 --- a/cookbook/tests/api/test_api_shopping_list_entryv2.py +++ b/cookbook/tests/api/test_api_shopping_list_entryv2.py @@ -7,14 +7,18 @@ from django.urls import reverse from django_scopes import scopes_disabled from pytest_factoryboy import LazyFixture, register -from cookbook.models import Food, ShoppingListEntry -from cookbook.tests.factories import ShoppingListEntryFactory +from cookbook.models import ShoppingListEntry +from cookbook.tests.factories import FoodFactory, ShoppingListEntryFactory LIST_URL = 'api:shoppinglistentry-list' DETAIL_URL = 'api:shoppinglistentry-detail' +# register(FoodFactory, 'food_1', space=LazyFixture('space_1')) +# register(FoodFactory, 'food_2', space=LazyFixture('space_1')) +# register(ShoppingListEntryFactory, 'sle_1', space=LazyFixture('space_1'), food=LazyFixture('food_1')) +# register(ShoppingListEntryFactory, 'sle_2', space=LazyFixture('space_1'), food=LazyFixture('food_2')) register(ShoppingListEntryFactory, 'sle_1', space=LazyFixture('space_1')) -register(ShoppingListEntryFactory, 'sle_2', space=LazyFixture('space_2')) +register(ShoppingListEntryFactory, 'sle_2', space=LazyFixture('space_1')) @pytest.mark.parametrize("arg", [ @@ -28,25 +32,25 @@ def test_list_permission(arg, request): assert c.get(reverse(LIST_URL)).status_code == arg[1] -def test_list_space(sle_1, sle_2, u1_s1, u1_s2, space_2): - assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 2 - assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0 +def test_list_space(sle_1, u1_s1, u1_s2, space_2): + assert json.loads(u1_s1.get(reverse(LIST_URL)).content)['count'] == 2 + assert json.loads(u1_s2.get(reverse(LIST_URL)).content)['count'] == 0 with scopes_disabled(): e = ShoppingListEntry.objects.first() e.space = space_2 e.save() - assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 1 - assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0 + assert json.loads(u1_s1.get(reverse(LIST_URL)).content)['count'] == 1 + assert json.loads(u1_s2.get(reverse(LIST_URL)).content)['count'] == 0 -def test_get_detail(sle_1): +def test_get_detail(u1_s1, sle_1): # r = u1_s1.get(reverse( # DETAIL_URL, # args={sle_1.id} # )) - # assert sle_1.id == 1 + # assert json.loads(r.content)['id'] == sle_1.id pass diff --git a/cookbook/tests/conftest.py b/cookbook/tests/conftest.py index 9c4892fe..e743687c 100644 --- a/cookbook/tests/conftest.py +++ b/cookbook/tests/conftest.py @@ -15,6 +15,7 @@ from cookbook.tests.factories import SpaceFactory register(SpaceFactory, 'space_1') register(SpaceFactory, 'space_2') +# TODO refactor user fixtures https://stackoverflow.com/questions/40966571/how-to-create-a-field-with-a-list-of-instances-in-factory-boy # hack from https://github.com/raphaelm/django-scopes to disable scopes for all fixtures # does not work on yield fixtures as only one yield can be used per fixture (i think) diff --git a/cookbook/tests/factories/__init__.py b/cookbook/tests/factories/__init__.py index 9f43fdbc..a71fd542 100644 --- a/cookbook/tests/factories/__init__.py +++ b/cookbook/tests/factories/__init__.py @@ -71,16 +71,16 @@ class FoodFactory(factory.django.DjangoModelFactory): """Food factory.""" name = factory.LazyAttribute(lambda x: faker.sentence(nb_words=3)) description = factory.LazyAttribute(lambda x: faker.sentence(nb_words=10)) - supermarket_category = factory.Maybe( - factory.LazyAttribute(lambda x: x.has_category), - yes_declaration=factory.SubFactory(SupermarketCategoryFactory), - no_declaration=None - ) - recipe = factory.Maybe( - factory.LazyAttribute(lambda x: x.has_recipe), - yes_declaration=factory.SubFactory('cookbook.tests.factories.RecipeFactory'), - no_declaration=None - ) + # supermarket_category = factory.Maybe( + # factory.LazyAttribute(lambda x: x.has_category), + # yes_declaration=factory.SubFactory(SupermarketCategoryFactory, space=factory.SelfAttribute('..space')), + # no_declaration=None + # ) + # recipe = factory.Maybe( + # factory.LazyAttribute(lambda x: x.has_recipe), + # yes_declaration=factory.SubFactory('cookbook.tests.factories.RecipeFactory', space=factory.SelfAttribute('..space')), + # no_declaration=None + # ) space = factory.SubFactory(SpaceFactory) class Params: @@ -114,8 +114,8 @@ class KeywordFactory(factory.django.DjangoModelFactory): class IngredientFactory(factory.django.DjangoModelFactory): """Ingredient factory.""" - food = factory.SubFactory(FoodFactory) - unit = factory.SubFactory(UnitFactory) + food = factory.SubFactory(FoodFactory, space=factory.SelfAttribute('..space')) + unit = factory.SubFactory(UnitFactory, space=factory.SelfAttribute('..space')) amount = factory.LazyAttribute(lambda x: faker.random_int(min=1, max=10)) note = factory.LazyAttribute(lambda x: faker.sentence(nb_words=5)) space = factory.SubFactory(SpaceFactory) @@ -130,7 +130,7 @@ class MealTypeFactory(factory.django.DjangoModelFactory): # icon = color = factory.LazyAttribute(lambda x: faker.safe_hex_color()) default = False - created_by = factory.SubFactory(UserFactory) + created_by = factory.SubFactory(UserFactory, space=factory.SelfAttribute('..space')) space = factory.SubFactory(SpaceFactory) class Meta: @@ -140,13 +140,13 @@ class MealTypeFactory(factory.django.DjangoModelFactory): class MealPlanFactory(factory.django.DjangoModelFactory): recipe = factory.Maybe( factory.LazyAttribute(lambda x: x.has_recipe), - yes_declaration=factory.SubFactory('cookbook.tests.factories.RecipeFactory'), + yes_declaration=factory.SubFactory('cookbook.tests.factories.RecipeFactory', space=factory.SelfAttribute('..space')), no_declaration=None ) servings = factory.LazyAttribute(lambda x: Decimal(faker.random_int(min=1, max=1000)/100)) title = factory.LazyAttribute(lambda x: faker.sentence(nb_words=5)) - created_by = factory.SubFactory(UserFactory) - meal_type = factory.SubFactory(MealTypeFactory) + created_by = factory.SubFactory(UserFactory, space=factory.SelfAttribute('..space')) + meal_type = factory.SubFactory(MealTypeFactory, space=factory.SelfAttribute('..space')) note = factory.LazyAttribute(lambda x: faker.paragraph()) date = factory.LazyAttribute(lambda x: faker.future_date()) space = factory.SubFactory(SpaceFactory) @@ -162,11 +162,11 @@ class ShoppingListRecipeFactory(factory.django.DjangoModelFactory): name = factory.LazyAttribute(lambda x: faker.sentence(nb_words=5)) recipe = factory.Maybe( factory.LazyAttribute(lambda x: x.has_recipe), - yes_declaration=factory.SubFactory('cookbook.tests.factories.RecipeFactory'), + yes_declaration=factory.SubFactory('cookbook.tests.factories.RecipeFactory', space=factory.SelfAttribute('..space')), no_declaration=None ) servings = factory.LazyAttribute(lambda x: faker.random_int(min=1, max=10)) - mealplan = factory.SubFactory(MealPlanFactory) + mealplan = factory.SubFactory(MealPlanFactory, space=factory.SelfAttribute('..space')) class Params: has_recipe = False @@ -177,22 +177,23 @@ class ShoppingListRecipeFactory(factory.django.DjangoModelFactory): class ShoppingListEntryFactory(factory.django.DjangoModelFactory): """ShoppingListEntry factory.""" - list_recipe = factory.Maybe( - factory.LazyAttribute(lambda x: x.has_mealplan), - yes_declaration=factory.SubFactory(ShoppingListRecipeFactory), - no_declaration=None - ) - food = factory.SubFactory(FoodFactory) - unit = factory.SubFactory(UnitFactory) - # ingredient = factory.SubFactory(IngredientFactory) - amount = factory.LazyAttribute(lambda x: Decimal(faker.random_int(min=1, max=10))/100) - order = 0 - checked = False - created_by = factory.SubFactory(UserFactory) - created_at = factory.LazyAttribute(lambda x: faker.past_date()) - completed_at = None - delay_until = None - space = factory.SubFactory(SpaceFactory) + + # list_recipe = factory.Maybe( + # factory.LazyAttribute(lambda x: x.has_mealplan), + # yes_declaration=factory.SubFactory(ShoppingListRecipeFactory, space=factory.SelfAttribute('..space')), + # no_declaration=None + # ) + food = factory.SubFactory(FoodFactory, space=factory.SelfAttribute('..space')) + # unit = factory.SubFactory(UnitFactory, space=factory.SelfAttribute('..space')) + # # ingredient = factory.SubFactory(IngredientFactory) + # amount = factory.LazyAttribute(lambda x: Decimal(faker.random_int(min=1, max=10))/100) + # order = 0 + # checked = False + # created_by = factory.SubFactory(UserFactory, space=factory.SelfAttribute('..space')) + # created_at = factory.LazyAttribute(lambda x: faker.past_date()) + # completed_at = None + # delay_until = None + space = factory.SubFactory('cookbook.tests.factories.SpaceFactory') class Params: has_mealplan = False @@ -209,14 +210,14 @@ class StepFactory(factory.django.DjangoModelFactory): # max_length=16 # ) instruction = factory.LazyAttribute(lambda x: ''.join(faker.paragraphs(nb=5))) - ingredients = factory.SubFactory(IngredientFactory) + ingredients = factory.SubFactory(IngredientFactory, space=factory.SelfAttribute('..space')) time = factory.LazyAttribute(lambda x: faker.random_int(min=1, max=1000)) order = 0 # file = models.ForeignKey('UserFile', on_delete=models.PROTECT, null=True, blank=True) show_as_header = True step_recipe = factory.Maybe( factory.LazyAttribute(lambda x: x.has_recipe), - yes_declaration=factory.SubFactory('cookbook.tests.factories.RecipeFactory'), + yes_declaration=factory.SubFactory('cookbook.tests.factories.RecipeFactory', space=factory.SelfAttribute('..space')), no_declaration=None ) space = factory.SubFactory(SpaceFactory) @@ -241,15 +242,15 @@ class RecipeFactory(factory.django.DjangoModelFactory): # file_path = models.CharField(max_length=512, default="", blank=True) # link = models.CharField(max_length=512, null=True, blank=True) # cors_link = models.CharField(max_length=1024, null=True, blank=True) - keywords = factory.SubFactory(KeywordFactory) - steps = factory.SubFactory(StepFactory) + keywords = factory.SubFactory(KeywordFactory, space=factory.SelfAttribute('..space')) + steps = factory.SubFactory(StepFactory, space=factory.SelfAttribute('..space')) working_time = factory.LazyAttribute(lambda x: faker.random_int(min=0, max=360)) waiting_time = factory.LazyAttribute(lambda x: faker.random_int(min=0, max=360)) internal = False # nutrition = models.ForeignKey( # NutritionInformation, blank=True, null=True, on_delete=models.CASCADE # ) - created_by = factory.SubFactory(UserFactory) + created_by = factory.SubFactory(UserFactory, space=factory.SelfAttribute('..space')) created_at = factory.LazyAttribute(lambda x: faker.date_this_decade()) # updated_at = models.DateTimeField(auto_now=True) space = factory.SubFactory(SpaceFactory) diff --git a/vue/src/apps/ShoppingListView/ShoppingListView.vue b/vue/src/apps/ShoppingListView/ShoppingListView.vue index 0ad2e824..a1885ef2 100644 --- a/vue/src/apps/ShoppingListView/ShoppingListView.vue +++ b/vue/src/apps/ShoppingListView/ShoppingListView.vue @@ -1,13 +1,16 @@ @@ -122,7 +125,7 @@ - + {{ s.name }} - + @@ -150,7 +153,7 @@

{{ $t("Shopping_Categories") }} - +

@@ -170,14 +173,9 @@
{{ $t("CategoryInstruction") }} - + -
- +
+ - + {{ categoryName(c) }} @@ -234,7 +221,7 @@
-
+
{{ $t("mealplan_autoadd_shopping") }}
@@ -375,8 +362,8 @@
-
-
+
+
{{ $t("Reset") }} {{ $t("Close") }}
@@ -396,7 +383,7 @@ - + @@ -575,7 +562,7 @@ export default { return groups }, defaultDelay() { - return getUserPreference("default_delay") || 2 + return Number(getUserPreference("default_delay")) || 2 }, formUnit() { let unit = this.Models.SHOPPING_LIST.create.form.unit @@ -620,10 +607,10 @@ export default { selected_supermarket(newVal, oldVal) { this.supermarket_categories_only = this.settings.filter_to_supermarket }, - "settings.filter_to_supermarket": function(newVal, oldVal) { + "settings.filter_to_supermarket": function (newVal, oldVal) { this.supermarket_categories_only = this.settings.filter_to_supermarket }, - "settings.shopping_auto_sync": function(newVal, oldVal) { + "settings.shopping_auto_sync": function (newVal, oldVal) { clearInterval(this.autosync_id) this.autosync_id = undefined if (!newVal) { @@ -678,32 +665,38 @@ export default { StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE) }) }, - resetFilters: function() { + resetFilters: function () { this.selected_supermarket = undefined this.supermarket_categories_only = this.settings.filter_to_supermarket this.show_undefined_categories = true this.group_by = "category" this.show_delay = false }, - delayThis: function(item) { + delayThis: function (item) { let entries = [] let promises = [] + let delay_date = new Date(Date.now() + this.delay * (60 * 60 * 1000)) + if (Array.isArray(item)) { + item = item.map((x) => { + return { ...x, delay_until: delay_date } + }) entries = item.map((x) => x.id) } else { + item.delay_until = delay_date entries = [item.id] } - let delay_date = new Date(Date.now() + this.delay * (60 * 60 * 1000)) entries.forEach((entry) => { promises.push(this.saveThis({ id: entry, delay_until: delay_date }, false)) }) Promise.all(promises).then(() => { StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE) + this.items = this.items.filter((x) => !entries.includes(x.id)) this.delay = this.defaultDelay }) }, - deleteRecipe: function(e, recipe) { + deleteRecipe: function (e, recipe) { let api = new ApiApiFactory() api.destroyShoppingListRecipe(recipe) .then((x) => { @@ -715,7 +708,7 @@ export default { StandardToasts.makeStandardToast(StandardToasts.FAIL_DELETE) }) }, - deleteThis: function(item) { + deleteThis: function (item) { let api = new ApiApiFactory() let entries = [] let promises = [] @@ -749,16 +742,16 @@ export default { this.supermarkets.filter((x) => x.id !== s.id).map((x) => (x.editmode = false)) } }, - foodName: function(value) { + foodName: function (value) { return value?.food?.name ?? value?.[0]?.food?.name ?? "" }, - getShoppingCategories: function() { + getShoppingCategories: function () { let api = new ApiApiFactory() api.listSupermarketCategorys().then((result) => { this.shopping_categories = result.data }) }, - getShoppingList: function(autosync = false) { + getShoppingList: function (autosync = false) { let params = {} params.supermarket = this.selected_supermarket @@ -788,23 +781,23 @@ export default { } }) }, - getSupermarkets: function() { + getSupermarkets: function () { let api = new ApiApiFactory() api.listSupermarkets().then((result) => { this.supermarkets = result.data }) }, - getThis: function(id) { + getThis: function (id) { return this.genericAPI(this.Models.SHOPPING_CATEGORY, this.Actions.FETCH, { id: id }) }, - ignoreThis: function(item) { + ignoreThis: function (item) { let food = { id: item?.[0]?.food.id ?? item.food.id, ignore_shopping: true, } this.updateFood(food, "ignore_shopping") }, - mergeShoppingList: function(data) { + mergeShoppingList: function (data) { this.items.map((x) => data.map((y) => { if (y.id === x.id) { @@ -815,7 +808,7 @@ export default { ) this.auto_sync_running = false }, - moveEntry: function(e, item) { + moveEntry: function (e, item) { if (!e) { makeToast(this.$t("Warning"), this.$t("NoCategory"), "warning") } @@ -826,7 +819,7 @@ export default { this.updateFood(food, "supermarket_category") this.shopcat = null }, - onHand: function(item) { + onHand: function (item) { let api = new ApiApiFactory() let food = { id: item?.[0]?.food.id ?? item?.food?.id, @@ -849,7 +842,7 @@ export default { this.shopcat = value?.food?.supermarket_category?.id ?? value?.[0]?.food?.supermarket_category?.id ?? undefined this.$refs.menu.open(e, value) }, - saveSettings: function() { + saveSettings: function () { let api = ApiApiFactory() api.partialUpdateUserPreference(this.settings.user, this.settings) .then((result) => { @@ -860,7 +853,7 @@ export default { StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE) }) }, - saveThis: function(thisItem, toast = true) { + saveThis: function (thisItem, toast = true) { let api = new ApiApiFactory() if (!thisItem?.id) { // if there is no item id assume it's a new item @@ -889,10 +882,10 @@ export default { }) } }, - sectionID: function(a, b) { + sectionID: function (a, b) { return (a + b).replace(/\W/g, "") }, - updateChecked: function(update) { + updateChecked: function (update) { // when checking a sub item don't refresh the screen until all entries complete but change class to cross out let promises = [] update.entries.forEach((x) => { @@ -911,7 +904,7 @@ export default { StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE) }) }, - updateFood: function(food, field) { + updateFood: function (food, field) { let api = new ApiApiFactory() let ignore_category if (field) { @@ -947,7 +940,7 @@ export default { this.getShoppingList() }) }, - addCategory: function() { + addCategory: function () { let api = new ApiApiFactory() api.createSupermarketCategory({ name: this.new_category.value }) .then((result) => { @@ -960,7 +953,7 @@ export default { StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE) }) }, - addSupermarket: function() { + addSupermarket: function () { let api = new ApiApiFactory() api.createSupermarket({ name: this.new_supermarket.value }) .then((result) => { @@ -978,7 +971,7 @@ export default { let apiClient = new ApiApiFactory() let supermarket = this.new_supermarket.value let temp_supermarkets = [...this.supermarkets] - const updateMoved = function(supermarket) { + const updateMoved = function (supermarket) { var promises = [] supermarket.category_to_supermarket.forEach((x, i) => { x.order = i diff --git a/vue/src/components/RecipeContextMenu.vue b/vue/src/components/RecipeContextMenu.vue index 26084695..024f9321 100644 --- a/vue/src/components/RecipeContextMenu.vue +++ b/vue/src/components/RecipeContextMenu.vue @@ -1,76 +1,40 @@