diff --git a/cookbook/admin.py b/cookbook/admin.py index 1207b9da..61966eb0 100644 --- a/cookbook/admin.py +++ b/cookbook/admin.py @@ -277,7 +277,7 @@ admin.site.register(RecipeBookEntry, RecipeBookEntryAdmin) class MealPlanAdmin(admin.ModelAdmin): - list_display = ('user', 'recipe', 'meal_type', 'date') + list_display = ('user', 'recipe', 'meal_type', 'from_date', 'to_date') @staticmethod def user(obj): diff --git a/cookbook/forms.py b/cookbook/forms.py index 3596d5fe..fb557dd9 100644 --- a/cookbook/forms.py +++ b/cookbook/forms.py @@ -323,49 +323,6 @@ class ImportRecipeForm(forms.ModelForm): } -# TODO deprecate -class MealPlanForm(forms.ModelForm): - def __init__(self, *args, **kwargs): - space = kwargs.pop('space') - super().__init__(*args, **kwargs) - self.fields['recipe'].queryset = Recipe.objects.filter(space=space).all() - self.fields['meal_type'].queryset = MealType.objects.filter(space=space).all() - self.fields['shared'].queryset = User.objects.filter(userpreference__space=space).all() - - def clean(self): - cleaned_data = super(MealPlanForm, self).clean() - - if cleaned_data['title'] == '' and cleaned_data['recipe'] is None: - raise forms.ValidationError( - _('You must provide at least a recipe or a title.') - ) - - return cleaned_data - - class Meta: - model = MealPlan - fields = ( - 'recipe', 'title', 'meal_type', 'note', - 'servings', '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') - } - - widgets = { - 'recipe': SelectWidget, - 'date': DateWidget, - 'shared': MultiSelectWidget - } - field_classes = { - 'recipe': SafeModelChoiceField, - 'meal_type': SafeModelChoiceField, - 'shared': SafeModelMultipleChoiceField, - } - - class InviteLinkForm(forms.ModelForm): def __init__(self, *args, **kwargs): user = kwargs.pop('user') diff --git a/cookbook/migrations/0201_rename_date_mealplan_from_date_mealplan_to_date.py b/cookbook/migrations/0201_rename_date_mealplan_from_date_mealplan_to_date.py new file mode 100644 index 00000000..343d758e --- /dev/null +++ b/cookbook/migrations/0201_rename_date_mealplan_from_date_mealplan_to_date.py @@ -0,0 +1,36 @@ +# Generated by Django 4.1.10 on 2023-09-08 12:20 + +from django.db import migrations, models +from django.db.models import F +from django_scopes import scopes_disabled + + +def apply_migration(apps, schema_editor): + with scopes_disabled(): + MealPlan = apps.get_model('cookbook', 'MealPlan') + MealPlan.objects.update(to_date=F('from_date')) + + +class Migration(migrations.Migration): + dependencies = [ + ('cookbook', '0200_alter_propertytype_options_remove_keyword_icon_and_more'), + ] + + operations = [ + migrations.RenameField( + model_name='mealplan', + old_name='date', + new_name='from_date', + ), + migrations.AddField( + model_name='mealplan', + name='to_date', + field=models.DateField(blank=True, null=True), + ), + migrations.RunPython(apply_migration), + migrations.AlterField( + model_name='mealplan', + name='to_date', + field=models.DateField(), + ), + ] diff --git a/cookbook/models.py b/cookbook/models.py index 875cb2b7..205845b1 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -991,7 +991,8 @@ class MealPlan(ExportModelOperationsMixin('meal_plan'), models.Model, Permission shared = models.ManyToManyField(User, blank=True, related_name='plan_share') meal_type = models.ForeignKey(MealType, on_delete=models.CASCADE) note = models.TextField(blank=True) - date = models.DateField() + from_date = models.DateField() + to_date = models.DateField() space = models.ForeignKey(Space, on_delete=models.CASCADE) objects = ScopedManager(space='space') @@ -1005,7 +1006,7 @@ class MealPlan(ExportModelOperationsMixin('meal_plan'), models.Model, Permission return self.meal_type.name def __str__(self): - return f'{self.get_label()} - {self.date} - {self.meal_type.name}' + return f'{self.get_label()} - {self.from_date} - {self.meal_type.name}' class ShoppingListRecipe(ExportModelOperationsMixin('shopping_list_recipe'), models.Model, PermissionModelMixin): diff --git a/cookbook/serializer.py b/cookbook/serializer.py index bf93ca72..c1dd0db0 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -993,7 +993,7 @@ class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer): model = MealPlan fields = ( 'id', 'title', 'recipe', 'servings', 'note', 'note_markdown', - 'date', 'meal_type', 'created_by', 'shared', 'recipe_name', + 'from_date', 'to_date', 'meal_type', 'created_by', 'shared', 'recipe_name', 'meal_type_name', 'shopping' ) read_only_fields = ('created_by',) diff --git a/cookbook/tests/api/test_api_meal_plan.py b/cookbook/tests/api/test_api_meal_plan.py index b27f6a0b..2a8243e8 100644 --- a/cookbook/tests/api/test_api_meal_plan.py +++ b/cookbook/tests/api/test_api_meal_plan.py @@ -12,6 +12,7 @@ from cookbook.tests.factories import RecipeFactory LIST_URL = 'api:mealplan-list' DETAIL_URL = 'api:mealplan-detail' + # NOTE: auto adding shopping list from meal plan is tested in test_shopping_recipe as tests are identical @@ -22,13 +23,13 @@ def meal_type(space_1, u1_s1): @pytest.fixture() def obj_1(space_1, recipe_1_s1, meal_type, u1_s1): - return MealPlan.objects.create(recipe=recipe_1_s1, space=space_1, meal_type=meal_type, date=datetime.now(), + return MealPlan.objects.create(recipe=recipe_1_s1, space=space_1, meal_type=meal_type, from_date=datetime.now(), to_date=datetime.now(), created_by=auth.get_user(u1_s1)) @pytest.fixture def obj_2(space_1, recipe_1_s1, meal_type, u1_s1): - return MealPlan.objects.create(recipe=recipe_1_s1, space=space_1, meal_type=meal_type, date=datetime.now(), + return MealPlan.objects.create(recipe=recipe_1_s1, space=space_1, meal_type=meal_type, from_date=datetime.now(), to_date=datetime.now(), created_by=auth.get_user(u1_s1)) @@ -109,7 +110,7 @@ def test_add(arg, request, u1_s2, recipe_1_s1, meal_type): r = c.post( reverse(LIST_URL), {'recipe': {'id': recipe_1_s1.id, 'name': recipe_1_s1.name, 'keywords': []}, 'meal_type': {'id': meal_type.id, 'name': meal_type.name}, - 'date': (datetime.now()).strftime("%Y-%m-%d"), 'servings': 1, 'title': 'test', 'shared': []}, + 'from_date': (datetime.now()).strftime("%Y-%m-%d"), 'to_date': (datetime.now()).strftime("%Y-%m-%d"), 'servings': 1, 'title': 'test', 'shared': []}, content_type='application/json' ) response = json.loads(r.content) @@ -151,7 +152,7 @@ def test_add_with_shopping(u1_s1, meal_type): r = u1_s1.post( reverse(LIST_URL), {'recipe': {'id': recipe.id, 'name': recipe.name, 'keywords': []}, 'meal_type': {'id': meal_type.id, 'name': meal_type.name}, - 'date': (datetime.now()).strftime("%Y-%m-%d"), 'servings': 1, 'title': 'test', 'shared': [], 'addshopping': True}, + 'from_date': (datetime.now()).strftime("%Y-%m-%d"), 'to_date': (datetime.now()).strftime("%Y-%m-%d"), 'servings': 1, 'title': 'test', 'shared': [], 'addshopping': True}, content_type='application/json' ) diff --git a/cookbook/tests/factories/__init__.py b/cookbook/tests/factories/__init__.py index d6c288c8..f1ce37a1 100644 --- a/cookbook/tests/factories/__init__.py +++ b/cookbook/tests/factories/__init__.py @@ -246,7 +246,8 @@ class MealPlanFactory(factory.django.DjangoModelFactory): meal_type = factory.SubFactory( MealTypeFactory, space=factory.SelfAttribute('..space')) note = factory.LazyAttribute(lambda x: faker.paragraph()) - date = factory.LazyAttribute(lambda x: faker.future_date()) + from_date = factory.LazyAttribute(lambda x: faker.future_date()) + to_date = factory.LazyAttribute(lambda x: faker.future_date()) space = factory.SubFactory(SpaceFactory) class Params: diff --git a/cookbook/urls.py b/cookbook/urls.py index 2fbbe325..03767562 100644 --- a/cookbook/urls.py +++ b/cookbook/urls.py @@ -166,7 +166,7 @@ urlpatterns = [ ] generic_models = ( - Recipe, RecipeImport, Storage, RecipeBook, MealPlan, SyncLog, Sync, + Recipe, RecipeImport, Storage, RecipeBook, SyncLog, Sync, Comment, RecipeBookEntry, ShoppingList, InviteLink, UserSpace, Space ) diff --git a/cookbook/views/api.py b/cookbook/views/api.py index 1f8766f0..738abeae 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -666,11 +666,11 @@ class MealPlanViewSet(viewsets.ModelViewSet): from_date = self.request.query_params.get('from_date', None) if from_date is not None: - queryset = queryset.filter(date__gte=from_date) + queryset = queryset.filter(to_date__gte=from_date) to_date = self.request.query_params.get('to_date', None) if to_date is not None: - queryset = queryset.filter(date__lte=to_date) + queryset = queryset.filter(to_date__lte=to_date) return queryset @@ -707,7 +707,7 @@ class AutoPlanViewSet(viewsets.ViewSet): args = {'recipe_id': recipe['id'], 'servings': servings, 'created_by': request.user, 'meal_type_id': serializer.validated_data['meal_type_id'], - 'note': '', 'date': day, 'space': request.space} + 'note': '', 'from_date': day, 'to_date': day, 'space': request.space} m = MealPlan(**args) meal_plans.append(m) @@ -1633,8 +1633,11 @@ def get_plan_ical(request, from_date, to_date): for p in queryset: event = Event() event['uid'] = p.id - event.add('dtstart', p.date) - event.add('dtend', p.date) + event.add('dtstart', p.from_date) + if p.to_date: + event.add('dtend', p.to_date) + else: + event.add('dtend', p.from_date) event['summary'] = f'{p.meal_type.name}: {p.get_label()}' event['description'] = p.note cal.add_component(event) diff --git a/cookbook/views/edit.py b/cookbook/views/edit.py index 50e87866..1bacc54a 100644 --- a/cookbook/views/edit.py +++ b/cookbook/views/edit.py @@ -8,7 +8,7 @@ from django.utils.translation import gettext as _ from django.views.generic import UpdateView from django.views.generic.edit import FormMixin -from cookbook.forms import CommentForm, ExternalRecipeForm, MealPlanForm, StorageForm, SyncForm +from cookbook.forms import CommentForm, ExternalRecipeForm, StorageForm, SyncForm from cookbook.helper.permission_helper import GroupRequiredMixin, OwnerRequiredMixin, group_required, above_space_limit from cookbook.models import (Comment, MealPlan, MealType, Recipe, RecipeImport, Storage, Sync, UserPreference) @@ -192,26 +192,6 @@ class ImportUpdate(GroupRequiredMixin, UpdateView): return context -class MealPlanUpdate(OwnerRequiredMixin, UpdateView, SpaceFormMixing): - template_name = "generic/edit_template.html" - model = MealPlan - form_class = MealPlanForm - - def get_success_url(self): - return reverse('view_plan_entry', kwargs={'pk': self.object.pk}) - - def get_form(self, form_class=None): - form = self.form_class(**self.get_form_kwargs()) - form.fields['meal_type'].queryset = MealType.objects \ - .filter(created_by=self.request.user).all() - return form - - def get_context_data(self, **kwargs): - context = super(MealPlanUpdate, self).get_context_data(**kwargs) - context['title'] = _("Meal-Plan") - return context - - class ExternalRecipeUpdate(GroupRequiredMixin, UpdateView, SpaceFormMixing): groups_required = ['user'] model = Recipe diff --git a/cookbook/views/new.py b/cookbook/views/new.py index 0bae4689..48dcf99d 100644 --- a/cookbook/views/new.py +++ b/cookbook/views/new.py @@ -12,7 +12,7 @@ from django.urls import reverse, reverse_lazy from django.utils.translation import gettext as _ from django.views.generic import CreateView -from cookbook.forms import ImportRecipeForm, InviteLinkForm, MealPlanForm, Storage, StorageForm +from cookbook.forms import ImportRecipeForm, InviteLinkForm, Storage, StorageForm from cookbook.helper.permission_helper import GroupRequiredMixin, group_required, above_space_limit from cookbook.models import (InviteLink, MealPlan, MealType, Recipe, RecipeBook, RecipeImport, ShareLink, Step, UserPreference, UserSpace) @@ -135,55 +135,3 @@ def create_new_external_recipe(request, import_id): return render(request, 'forms/edit_import_recipe.html', {'form': form}) -class MealPlanCreate(GroupRequiredMixin, CreateView, SpaceFormMixing): - groups_required = ['user'] - template_name = "generic/new_template.html" - model = MealPlan - form_class = MealPlanForm - success_url = reverse_lazy('view_plan') - - def get_form(self, form_class=None): - form = self.form_class(**self.get_form_kwargs()) - form.fields['meal_type'].queryset = MealType.objects.filter(created_by=self.request.user, - space=self.request.space).all() - return form - - def get_initial(self): - return dict( - meal_type=( - self.request.GET['meal'] - if 'meal' in self.request.GET - else None - ), - date=( - datetime.strptime(self.request.GET['date'], '%Y-%m-%d') - if 'date' in self.request.GET - else None - ), - shared=( - self.request.user.userpreference.plan_share.all() - if self.request.user.userpreference.plan_share - else None - ) - ) - - def form_valid(self, form): - obj = form.save(commit=False) - obj.created_by = self.request.user - obj.space = self.request.space - obj.save() - return HttpResponseRedirect(reverse('view_plan')) - - def get_context_data(self, **kwargs): - context = super(MealPlanCreate, self).get_context_data(**kwargs) - context['title'] = _("Meal-Plan") - - recipe = self.request.GET.get('recipe') - if recipe: - if re.match(r'^([0-9])+$', recipe): - if Recipe.objects.filter(pk=int(recipe), space=self.request.space).exists(): - context['default_recipe'] = Recipe.objects.get(pk=int(recipe), space=self.request.space) - - return context - - diff --git a/vue/src/apps/MealPlanView/MealPlanView.vue b/vue/src/apps/MealPlanView/MealPlanView.vue index 1473d708..3d245e3c 100644 --- a/vue/src/apps/MealPlanView/MealPlanView.vue +++ b/vue/src/apps/MealPlanView/MealPlanView.vue @@ -1,219 +1,165 @@