diff --git a/cookbook/helper/image_processing.py b/cookbook/helper/image_processing.py index d9a334acb..552bf1312 100644 --- a/cookbook/helper/image_processing.py +++ b/cookbook/helper/image_processing.py @@ -38,10 +38,12 @@ def get_filetype(name): # TODO this whole file needs proper documentation, refactoring, and testing # TODO also add env variable to define which images sizes should be compressed -def handle_image(request, image_object, filetype='.jpeg'): +# filetype argument can not be optional, otherwise this function will treat all images as if they were a jpeg +# Because it's no longer optional, no reason to return it +def handle_image(request, image_object, filetype): if (image_object.size / 1000) > 500: # if larger than 500 kb compress if filetype == '.jpeg' or filetype == '.jpg': - return rescale_image_jpeg(image_object), filetype + return rescale_image_jpeg(image_object) if filetype == '.png': - return rescale_image_png(image_object), filetype - return image_object, filetype + return rescale_image_png(image_object) + return image_object diff --git a/cookbook/helper/ingredient_parser.py b/cookbook/helper/ingredient_parser.py index 8c07635af..593dea7e3 100644 --- a/cookbook/helper/ingredient_parser.py +++ b/cookbook/helper/ingredient_parser.py @@ -221,7 +221,7 @@ class IngredientParser: # some people/languages put amount and unit at the end of the ingredient string # if something like this is detected move it to the beginning so the parser can handle it - if re.search(r'^([A-z])+(.)*[1-9](\d)*\s([A-z])+', ingredient): + if len(ingredient) < 1000 and re.search(r'^([A-z])+(.)*[1-9](\d)*\s([A-z])+', ingredient): match = re.search(r'[1-9](\d)*\s([A-z])+', ingredient) print(f'reording from {ingredient} to {ingredient[match.start():match.end()] + " " + ingredient.replace(ingredient[match.start():match.end()], "")}') ingredient = ingredient[match.start():match.end()] + ' ' + ingredient.replace(ingredient[match.start():match.end()], '') diff --git a/cookbook/integration/integration.py b/cookbook/integration/integration.py index 6493c266d..c3dc9dfb7 100644 --- a/cookbook/integration/integration.py +++ b/cookbook/integration/integration.py @@ -253,7 +253,7 @@ class Integration: :param image_file: ByteIO stream containing the image :param filetype: type of file to write bytes to, default to .jpeg if unknown """ - recipe.image = File(handle_image(self.request, File(image_file, name='image'), filetype=filetype)[0], name=f'{uuid.uuid4()}_{recipe.pk}{filetype}') + recipe.image = File(handle_image(self.request, File(image_file, name='image'), filetype=filetype), name=f'{uuid.uuid4()}_{recipe.pk}{filetype}') recipe.save() def get_recipe_from_file(self, file): diff --git a/cookbook/locale/de/LC_MESSAGES/django.po b/cookbook/locale/de/LC_MESSAGES/django.po index 0f9c05294..ad6f1e0ab 100644 --- a/cookbook/locale/de/LC_MESSAGES/django.po +++ b/cookbook/locale/de/LC_MESSAGES/django.po @@ -2467,7 +2467,7 @@ msgid "" msgstr "" "Das direkte ausliefern von Mediendateien mit gunicorn/python ist nicht " "empfehlenswert! Bitte folge den beschriebenen Schritten hier, um Ihre " +"://github.com/vabene1111/recipes/releases/tag/0.8.1\">hier, um Ihre " "Installation zu aktualisieren.\n" " " diff --git a/cookbook/templates/base.html b/cookbook/templates/base.html index 625a96fec..9ca099ba1 100644 --- a/cookbook/templates/base.html +++ b/cookbook/templates/base.html @@ -337,6 +337,12 @@ {% endif %} +{% if HOSTED and request.space.max_recipes == 10 %} +
+ {% trans 'You are using the free version of Tandor' %} {% trans 'Upgrade Now' %} +
+{% endif %} +
diff --git a/cookbook/tests/api/test_api_ingredient.py b/cookbook/tests/api/test_api_ingredient.py index cde3d65a9..c3182b1e2 100644 --- a/cookbook/tests/api/test_api_ingredient.py +++ b/cookbook/tests/api/test_api_ingredient.py @@ -23,8 +23,8 @@ def test_list_permission(arg, request): def test_list_space(recipe_1_s1, u1_s1, u1_s2, space_2): - assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 10 - assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0 + assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)['results']) == 10 + assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)['results']) == 0 with scopes_disabled(): recipe_1_s1.space = space_2 @@ -32,8 +32,8 @@ def test_list_space(recipe_1_s1, u1_s1, u1_s2, space_2): Step.objects.update(space=Subquery(Step.objects.filter(pk=OuterRef('pk')).values('recipe__space')[:1])) Ingredient.objects.update(space=Subquery(Ingredient.objects.filter(pk=OuterRef('pk')).values('step__recipe__space')[:1])) - assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 0 - assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 10 + assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)['results']) == 0 + assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)['results']) == 10 @pytest.mark.parametrize("arg", [ diff --git a/cookbook/tests/api/test_api_shopping_recipe.py b/cookbook/tests/api/test_api_shopping_recipe.py index 9732250d5..bbf3cb8fb 100644 --- a/cookbook/tests/api/test_api_shopping_recipe.py +++ b/cookbook/tests/api/test_api_shopping_recipe.py @@ -230,4 +230,4 @@ def test_shopping_with_header_ingredient(u1_s1, recipe): # recipe.step_set.first().ingredient_set.add(IngredientFactory(ingredients__header=1)) u1_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id})) assert len(json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 10 - assert len(json.loads(u1_s1.get(reverse('api:ingredient-list')).content)) == 11 + assert len(json.loads(u1_s1.get(reverse('api:ingredient-list')).content)['results']) == 11 diff --git a/cookbook/views/api.py b/cookbook/views/api.py index 4b4c8980c..b07aaf35f 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -1,5 +1,6 @@ import io import json +import mimetypes import re import uuid from collections import OrderedDict @@ -773,14 +774,16 @@ class RecipeViewSet(viewsets.ModelViewSet): if serializer.is_valid(): serializer.save() image = None + filetype = ".jpeg" # fall-back to .jpeg, even if wrong, at least users will know it's an image and most image viewers can open it correctly anyways if 'image' in serializer.validated_data: image = obj.image + filetype = mimetypes.guess_extension(serializer.validated_data['image'].content_type) or filetype elif 'image_url' in serializer.validated_data: try: response = requests.get(serializer.validated_data['image_url']) image = File(io.BytesIO(response.content)) - print('test') + filetype = mimetypes.guess_extension(response.headers['content-type']) or filetype except UnidentifiedImageError as e: print(e) pass @@ -792,7 +795,7 @@ class RecipeViewSet(viewsets.ModelViewSet): pass if image is not None: - img, filetype = handle_image(request, image) + img = handle_image(request, image, filetype) obj.image = File(img, name=f'{uuid.uuid4()}_{obj.pk}{filetype}') obj.save() return Response(serializer.data) diff --git a/vue/src/apps/ImportView/ImportView.vue b/vue/src/apps/ImportView/ImportView.vue index 28993fae2..c76c1f190 100644 --- a/vue/src/apps/ImportView/ImportView.vue +++ b/vue/src/apps/ImportView/ImportView.vue @@ -218,8 +218,7 @@ :href="resolveDjangoUrl('view_recipe',r.id)">{{ r.name }} - Imported - + {{ $t('Imported') }}
@@ -230,8 +229,7 @@
{{ u }} - Failed - + {{ $t('Failure') }}
diff --git a/vue/src/apps/IngredientEditorView/IngredientEditorView.vue b/vue/src/apps/IngredientEditorView/IngredientEditorView.vue index 35b1c5ba1..531f159ca 100644 --- a/vue/src/apps/IngredientEditorView/IngredientEditorView.vue +++ b/vue/src/apps/IngredientEditorView/IngredientEditorView.vue @@ -1,23 +1,37 @@ @@ -153,7 +197,7 @@ Vue.use(BootstrapVue) export default { name: "IngredientEditorView", mixins: [ApiMixin, ResolveUrlMixin], - components: {BetaWarning, LoadingSpinner, GenericMultiselect, GenericModalForm}, + components: {LoadingSpinner, GenericMultiselect, GenericModalForm}, data() { return { ingredients: [], @@ -202,7 +246,7 @@ export default { if (this.unit !== null) { params.query.unit = this.unit.id } - apiClient.listIngredients(this.current_page, this.page_size,params).then(result => { + apiClient.listIngredients(this.current_page, this.page_size, params).then(result => { this.ingredients = result.data.results this.total_object_count = result.data.count this.loading = false diff --git a/vue/src/apps/ShoppingListView/ShoppingListView.vue b/vue/src/apps/ShoppingListView/ShoppingListView.vue index d418a3ed8..2e0277949 100644 --- a/vue/src/apps/ShoppingListView/ShoppingListView.vue +++ b/vue/src/apps/ShoppingListView/ShoppingListView.vue @@ -112,21 +112,21 @@
-
-
+
{{ $t("Completed") }}
-
+
-
+
-
+
category -> food group -> entries) + // ordering/sorting is definied by the order in which categories are added to the sections array (even trough the dev console does not show it like this) function getKey(item, group_by, x) { switch (group_by) { case "category": @@ -879,10 +881,12 @@ export default { } var groups = {false: {}, true: {}} // force unchecked to always be first + // TODO: make nulls_first a user setting + // add undefined group to both the checked and non checked + groups.false[this.$t("Undefined")] = {} + groups.true[this.$t("Undefined")] = {} + // category order is defined by order of insertion into groups variable if (this.ui.selected_supermarket) { - // TODO: make nulls_first a user setting - groups.false[this.$t("Undefined")] = {} - groups.true[this.$t("Undefined")] = {} let super_cats = this.supermarkets .filter((x) => x.id === this.ui.selected_supermarket) .map((x) => x.category_to_supermarket) @@ -978,7 +982,7 @@ export default { watch: { ui: { handler() { - this.$cookies.set(SETTINGS_COOKIE_NAME, {ui: this.ui, settings: {entrymode: this.entrymode}}) + this.$cookies.set(SETTINGS_COOKIE_NAME, {ui: this.ui, settings: {entrymode: this.entrymode}}, "100y") if (this.entrymode) { this.$nextTick(function () { this.setFocus() @@ -989,7 +993,7 @@ export default { }, entrymode: { handler() { - this.$cookies.set(SETTINGS_COOKIE_NAME, {ui: this.ui, settings: {entrymode: this.entrymode}}) + this.$cookies.set(SETTINGS_COOKIE_NAME, {ui: this.ui, settings: {entrymode: this.entrymode}}, "100y") if (this.entrymode) { document.getElementById('shoppinglist').scrollTop = 0 this.$nextTick(function () { @@ -1069,7 +1073,7 @@ export default { if (this.new_item.ingredient !== "" && this.new_item.ingredient !== undefined) { this.genericPostAPI("api_ingredient_from_string", {text: this.new_item.ingredient}).then((result) => { let unit = null - if (result.data.unit !== "") { + if (result.data.unit !== null) { unit = {name: result.data.unit} } @@ -1129,9 +1133,10 @@ export default { promises.push(this.saveThis({id: entry, delay_until: delay_date}, false)) }) Promise.all(promises).then(() => { - StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE) this.items = this.items.filter((x) => !entries.includes(x.id)) this.delay = this.defaultDelay + }).catch(err => { + StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err) }) }, deleteRecipe: function (e, recipe) { diff --git a/vue/src/locales/en.json b/vue/src/locales/en.json index 064be63ad..3f7ff7646 100644 --- a/vue/src/locales/en.json +++ b/vue/src/locales/en.json @@ -368,6 +368,7 @@ "no_pinned_recipes": "You have no pinned recipes!", "Planned": "Planned", "Pinned": "Pinned", + "Imported": "Imported", "Quick actions": "Quick actions", "Ratings": "Ratings", "Internal": "Internal",