diff --git a/cookbook/forms.py b/cookbook/forms.py index 856010f90..a4301eb0a 100644 --- a/cookbook/forms.py +++ b/cookbook/forms.py @@ -264,11 +264,12 @@ class MealPlanForm(forms.ModelForm): class Meta: model = MealPlan - fields = ('recipe', 'title', 'meal_type', 'note', 'date', 'shared') + fields = ('recipe', 'title', 'meal_type', 'note', 'recipe_multiplier', 'date', 'shared') help_texts = { 'shared': _('You can list default users to share recipes with in the settings.'), - 'note': _('You can use markdown to format this field. See the docs here') + 'note': _('You can use markdown to format this field. See the docs here'), + 'recipe_multiplier': _('Scaling factor for recipe.') } widgets = {'recipe': SelectWidget, 'date': DateWidget, 'shared': MultiSelectWidget} diff --git a/cookbook/migrations/0086_auto_20200929_1143.py b/cookbook/migrations/0086_auto_20200929_1143.py new file mode 100644 index 000000000..582aa024a --- /dev/null +++ b/cookbook/migrations/0086_auto_20200929_1143.py @@ -0,0 +1,24 @@ +# Generated by Django 3.0.7 on 2020-09-29 09:43 + +import datetime +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cookbook', '0085_auto_20200922_1235'), + ] + + operations = [ + migrations.AddField( + model_name='mealplan', + name='recipe_multiplier', + field=models.IntegerField(default=1), + ), + migrations.AlterField( + model_name='invitelink', + name='valid_until', + field=models.DateField(default=datetime.date(2020, 10, 13)), + ), + ] diff --git a/cookbook/migrations/0087_auto_20200929_1152.py b/cookbook/migrations/0087_auto_20200929_1152.py new file mode 100644 index 000000000..358fe3553 --- /dev/null +++ b/cookbook/migrations/0087_auto_20200929_1152.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.7 on 2020-09-29 09:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cookbook', '0086_auto_20200929_1143'), + ] + + operations = [ + migrations.AlterField( + model_name='mealplan', + name='recipe_multiplier', + field=models.DecimalField(decimal_places=4, default=1, max_digits=4), + ), + migrations.AlterField( + model_name='shoppinglistrecipe', + name='multiplier', + field=models.DecimalField(decimal_places=4, default=1, max_digits=4), + ), + ] diff --git a/cookbook/migrations/0088_auto_20200929_1202.py b/cookbook/migrations/0088_auto_20200929_1202.py new file mode 100644 index 000000000..548f6c005 --- /dev/null +++ b/cookbook/migrations/0088_auto_20200929_1202.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.7 on 2020-09-29 10:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cookbook', '0087_auto_20200929_1152'), + ] + + operations = [ + migrations.AlterField( + model_name='mealplan', + name='recipe_multiplier', + field=models.DecimalField(decimal_places=4, default=1, max_digits=8), + ), + migrations.AlterField( + model_name='shoppinglistrecipe', + name='multiplier', + field=models.DecimalField(decimal_places=4, default=1, max_digits=8), + ), + ] diff --git a/cookbook/models.py b/cookbook/models.py index 45833e2a6..06d858a22 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -254,6 +254,7 @@ class MealType(models.Model): class MealPlan(models.Model): recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, blank=True, null=True) + recipe_multiplier = models.DecimalField(default=1, max_digits=8, decimal_places=4) title = models.CharField(max_length=64, blank=True, default='') created_by = models.ForeignKey(User, on_delete=models.CASCADE) shared = models.ManyToManyField(User, blank=True, related_name='plan_share') @@ -275,7 +276,7 @@ class MealPlan(models.Model): class ShoppingListRecipe(models.Model): recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, null=True, blank=True) - multiplier = models.IntegerField(default=1) + multiplier = models.DecimalField(default=1, max_digits=8, decimal_places=4) def __str__(self): return f'Shopping list recipe {self.id} - {self.recipe}' diff --git a/cookbook/serializer.py b/cookbook/serializer.py index 78323aafa..bd6ed59ad 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -192,17 +192,19 @@ class MealPlanSerializer(serializers.ModelSerializer): recipe_name = serializers.ReadOnlyField(source='recipe.name') meal_type_name = serializers.ReadOnlyField(source='meal_type.name') note_markdown = serializers.SerializerMethodField('get_note_markdown') + recipe_multiplier = CustomDecimalField() def get_note_markdown(self, obj): return markdown(obj.note) class Meta: model = MealPlan - fields = ('id', 'title', 'recipe', 'note', 'note_markdown', 'date', 'meal_type', 'created_by', 'shared', 'recipe_name', 'meal_type_name') + fields = ('id', 'title', 'recipe', 'recipe_multiplier', 'note', 'note_markdown', 'date', 'meal_type', 'created_by', 'shared', 'recipe_name', 'meal_type_name') class ShoppingListRecipeSerializer(serializers.ModelSerializer): recipe_name = serializers.ReadOnlyField(source='recipe.name') + multiplier = CustomDecimalField() class Meta: model = ShoppingListRecipe diff --git a/cookbook/templates/meal_plan.html b/cookbook/templates/meal_plan.html index b84e444c7..22ae397f7 100644 --- a/cookbook/templates/meal_plan.html +++ b/cookbook/templates/meal_plan.html @@ -126,6 +126,9 @@ class="text-muted">{% trans 'You can use markdown to format this field. See the docs here' %}

+ +
{ console.log("getPlanEntries error: ", err); - this.makeToast('{% trans 'Error' %}','{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger') + this.makeToast('{% trans 'Error' %}', '{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger') }) }, getPlanTypes: function () { @@ -401,7 +405,7 @@ } }).catch((err) => { console.log("getPlanTypes error: ", err); - this.makeToast('{% trans 'Error' %}','{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger') + this.makeToast('{% trans 'Error' %}', '{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger') }) }, buildGrid: function () { @@ -451,7 +455,7 @@ this.recipes = response.data; }).catch((err) => { console.log("getRecipes error: ", err); - this.makeToast('{% trans 'Error' %}','{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger') + this.makeToast('{% trans 'Error' %}', '{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger') }) }, getMdNote: function () { @@ -464,18 +468,18 @@ this.recipes = response.data; }).catch((err) => { console.log("getRecipes error: ", err); - this.makeToast('{% trans 'Error' %}','{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger') + this.makeToast('{% trans 'Error' %}', '{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger') }) }, updateUserNames: function () { return this.$http.get("{% url 'api:username-list' %}?filter_list=[" + this.user_id_update + ']').then((response) => { for (let u of response.data) { - this.$set(this.user_names, u.id, u.username); + this.$set(this.user_names, u.id, u.username); } }).catch((err) => { console.log("updateUserNames error: ", err); - this.makeToast('{% trans 'Error' %}','{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger') + this.makeToast('{% trans 'Error' %}', '{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger') }) }, dragChanged: function (date, meal_type, evt) { @@ -501,7 +505,7 @@ this.$http.put(`{% url 'api:mealplan-list' %}${plan_entry.id}/`, plan_entry).then((response) => { }).catch((err) => { console.log("dragChanged update error", err); - this.makeToast('{% trans 'Error' %}','{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger') + this.makeToast('{% trans 'Error' %}', '{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger') }) } } @@ -512,7 +516,7 @@ this.meal_plan[entry.meal_type_name].days[entry.date].items = this.meal_plan[entry.meal_type_name].days[entry.date].items.filter(item => item !== entry) }).catch((err) => { console.log("deleteEntry error: ", err); - this.makeToast('{% trans 'Error' %}','{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger') + this.makeToast('{% trans 'Error' %}', '{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger') }) }, updatePlanTypes: function () { @@ -526,14 +530,14 @@ promise_list.push(this.$http.post("{% url 'api:mealtype-list' %}", x).then((response) => { }).catch((err) => { console.log("updatePlanTypes create error: ", err); - this.makeToast('{% trans 'Error' %}','{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger') + this.makeToast('{% trans 'Error' %}', '{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger') })) } else if (x.delete) { if (x.id !== undefined) { promise_list.push(this.$http.delete(`{% url 'api:mealtype-list' %}${x.id}/`, x).then((response) => { }).catch((err) => { console.log("updatePlanTypes delete error: ", err); - this.makeToast('{% trans 'Error' %}','{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger') + this.makeToast('{% trans 'Error' %}', '{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger') })) } } else { @@ -541,7 +545,7 @@ }).catch((err) => { console.log("updatePlanTypes update error: ", err); - this.makeToast('{% trans 'Error' %}','{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger') + this.makeToast('{% trans 'Error' %}', '{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger') })) } } @@ -556,14 +560,21 @@ } }, cloneRecipe: function (recipe) { - return { + let r = { id: Math.round(Math.random() * 1000) + 10000, recipe: recipe.id, recipe_name: recipe.name, + recipe_multiplier: (this.new_note_multiplier > 1) ? this.new_note_multiplier : 1, title: this.new_note_title, note: this.new_note_text, is_new: true } + + this.new_note_title = '' + this.new_note_text = '' + this.new_note_multiplier = '' + + return r }, cloneNote: function () { let new_entry = { @@ -579,6 +590,7 @@ this.new_note_title = '' this.new_note_text = '' + this.new_note_multiplier = '' return new_entry }, planElementName: function (element) { @@ -618,10 +630,10 @@ let first = true for (let se of this.shopping_list) { if (first) { - url += `?r=${se.recipe}` + url += `?r=[${se.recipe},${se.recipe_multiplier}]` first = false } else { - url += `&r=${se.recipe}` + url += `&r=[${se.recipe},${se.recipe_multiplier}]` } } return url diff --git a/cookbook/templates/shopping_list.html b/cookbook/templates/shopping_list.html index 37881d69e..6b71c738e 100644 --- a/cookbook/templates/shopping_list.html +++ b/cookbook/templates/shopping_list.html @@ -183,13 +183,6 @@
-
-
- -
-
@@ -221,32 +214,42 @@
- {% trans 'Export' %} - - -
-
- -
- -
- -
-
- - - -
-
- - -
+
+
+ {% trans 'Export' %} + +
+
+ + + +
+
+ +
+ +
+ +
+
+ + + +
+
+ + +
+ @@ -339,12 +342,25 @@ mounted: function () { this.loadShoppingList() + {% if recipes %} + this.loading = true + this.edit_mode = true + let loadingRecipes = [] + {% for r in recipes %} + loadingRecipes.push(this.loadInitialRecipe({{ r.recipe }}, {{ r.multiplier }})) + {% endfor %} + + Promise.allSettled(loadingRecipes).then(() => { + this.loading = false + }) + {% endif %} + {% if request.user.userpreference.shopping_auto_sync > 0 %} setInterval(() => { if ((this.shopping_list_id !== null) && !this.edit_mode) { this.loadShoppingList(true) } - }, {{ request.user.userpreference.shopping_auto_sync }} * 1000 ) + }, {% widthratio request.user.userpreference.shopping_auto_sync 1 1000 %}) {% endif %} }, methods: { @@ -365,6 +381,14 @@ solid: true }) }, + loadInitialRecipe: function (recipe, multiplier) { + return this.$http.get('{% url 'api:recipe-detail' 123456 %}'.replace('123456', recipe)).then((response) => { + this.addRecipeToList(response.data, multiplier) + }).catch((err) => { + console.log("getRecipes error: ", err); + this.makeToast('{% trans 'Error' %}', '{% trans 'There was an error loading a resource!' %}' + err.bodyText, 'danger') + }) + }, loadShoppingList: function (autosync = false) { if (this.shopping_list_id) { @@ -433,6 +457,8 @@ this.shopping_list = response.body this.shopping_list_id = this.shopping_list.id + + window.history.pushState('shopping_list', '{% trans 'Shopping List' %}', "{% url 'view_shopping' 123456 %}".replace('123456', this.shopping_list_id)); }).catch((err) => { console.log(err) this.makeToast('{% trans 'Error' %}', '{% trans 'There was an error creating a resource!' %}' + err.bodyText, 'danger') @@ -511,13 +537,13 @@ getRecipeUrl: function (id) { //TODO generic function that can be reused else were return '{% url 'view_recipe' 123456 %}'.replace('123456', id) }, - addRecipeToList: function (recipe) { + addRecipeToList: function (recipe, multiplier = 1) { let slr = { "created": true, "id": Math.random() * 1000, "recipe": recipe.id, "recipe_name": recipe.name, - "multiplier": 1 + "multiplier": multiplier } this.shopping_list.recipes.push(slr) diff --git a/cookbook/views/views.py b/cookbook/views/views.py index e4a3ef37e..183e222e9 100644 --- a/cookbook/views/views.py +++ b/cookbook/views/views.py @@ -165,7 +165,17 @@ def meal_plan_entry(request, pk): @group_required('user') def shopping_list(request, pk=None): - return render(request, 'shopping_list.html', {'shopping_list_id': pk}) + raw_list = request.GET.getlist('r') + + recipes = [] + for r in raw_list: + r = r.replace('[', '').replace(']', '') + if re.match(r'^([1-9])+,([1-9])+[.]*([1-9])*$', r): + rid, multiplier = r.split(',') + if recipe := Recipe.objects.filter(pk=int(rid)).first(): + recipes.append({'recipe': recipe.id, 'multiplier': multiplier}) + + return render(request, 'shopping_list.html', {'shopping_list_id': pk, 'recipes': recipes}) @group_required('guest')