From ad163509b4ce6eacaf49f267cbcf54ab8924f45c Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Fri, 19 Feb 2021 18:09:42 +0100 Subject: [PATCH] updated edit and added space find methods --- cookbook/helper/permission_helper.py | 20 ++++-- cookbook/helper/scope_middleware.py | 3 +- cookbook/models.py | 103 ++++++++++++++++----------- cookbook/views/edit.py | 52 +++++--------- cookbook/views/import_export.py | 2 +- 5 files changed, 96 insertions(+), 84 deletions(-) diff --git a/cookbook/helper/permission_helper.py b/cookbook/helper/permission_helper.py index f6db4d81..21e4c196 100644 --- a/cookbook/helper/permission_helper.py +++ b/cookbook/helper/permission_helper.py @@ -1,6 +1,9 @@ """ Source: https://djangosnippets.org/snippets/1703/ """ +from django.views.generic.detail import SingleObjectTemplateResponseMixin +from django.views.generic.edit import ModelFormMixin + from cookbook.models import ShareLink from django.contrib import messages from django.contrib.auth.decorators import user_passes_test @@ -131,11 +134,11 @@ class GroupRequiredMixin(object): def dispatch(self, request, *args, **kwargs): if not has_group_permission(request.user, self.groups_required): - messages.add_message( - request, - messages.ERROR, - _('You do not have the required permissions to view this page!') # noqa: E501 - ) + messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!')) + return HttpResponseRedirect(reverse_lazy('index')) + + if self.get_object().get_space() != request.space: + messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!')) return HttpResponseRedirect(reverse_lazy('index')) return super(GroupRequiredMixin, self).dispatch(request, *args, **kwargs) @@ -162,8 +165,11 @@ class OwnerRequiredMixin(object): ) return HttpResponseRedirect(reverse('index')) - return super(OwnerRequiredMixin, self) \ - .dispatch(request, *args, **kwargs) + if self.get_object().get_space() != request.space: + messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!')) + return HttpResponseRedirect(reverse_lazy('index')) + + return super(OwnerRequiredMixin, self).dispatch(request, *args, **kwargs) # Django Rest Framework Permission classes diff --git a/cookbook/helper/scope_middleware.py b/cookbook/helper/scope_middleware.py index 595a5f91..0bf415b8 100644 --- a/cookbook/helper/scope_middleware.py +++ b/cookbook/helper/scope_middleware.py @@ -1,4 +1,4 @@ -from django_scopes import scope +from django_scopes import scope, scopes_disabled class ScopeMiddleware: @@ -9,6 +9,7 @@ class ScopeMiddleware: if request.user.is_authenticated: request.space = request.user.userpreference.space + #with scopes_disabled(): with scope(space=request.space): return self.get_response(request) else: diff --git a/cookbook/models.py b/cookbook/models.py index fd6ca6ce..dd3b1098 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -9,7 +9,6 @@ from django.core.validators import MinLengthValidator from django.db import models from django.utils import timezone from django.utils.translation import gettext as _ -from django_random_queryset import RandomManager from django_scopes import ScopedManager from recipes.settings import (COMMENT_PREF_DEFAULT, FRACTION_PREF_DEFAULT, @@ -30,12 +29,26 @@ def get_model_name(model): return ('_'.join(re.findall('[A-Z][^A-Z]*', model.__name__))).lower() +class PermissionModelMixin: + def get_owner(self): + if getattr(self, 'created_by', None): + return self.created_by + if getattr(self, 'user', None): + return self.user + return None + + def get_space(self): + if getattr(self, 'space', None): + return self.space + raise NotImplementedError('get space for method not implemented and standard fields not available') + + class Space(models.Model): name = models.CharField(max_length=128, default='Default') message = models.CharField(max_length=512, default='', blank=True) -class UserPreference(models.Model): +class UserPreference(models.Model, PermissionModelMixin): # Themes BOOTSTRAP = 'BOOTSTRAP' DARKLY = 'DARKLY' @@ -115,7 +128,7 @@ class UserPreference(models.Model): return str(self.user) -class Storage(models.Model): +class Storage(models.Model, PermissionModelMixin): DROPBOX = 'DB' NEXTCLOUD = 'NEXTCLOUD' LOCAL = 'LOCAL' @@ -139,7 +152,7 @@ class Storage(models.Model): return self.name -class Sync(models.Model): +class Sync(models.Model, PermissionModelMixin): storage = models.ForeignKey(Storage, on_delete=models.PROTECT) path = models.CharField(max_length=512, default="") active = models.BooleanField(default=True) @@ -154,7 +167,7 @@ class Sync(models.Model): return self.path -class SupermarketCategory(models.Model): +class SupermarketCategory(models.Model, PermissionModelMixin): name = models.CharField(unique=True, max_length=128, validators=[MinLengthValidator(1)]) description = models.TextField(blank=True, null=True) @@ -165,7 +178,7 @@ class SupermarketCategory(models.Model): return self.name -class Supermarket(models.Model): +class Supermarket(models.Model, PermissionModelMixin): name = models.CharField(unique=True, max_length=128, validators=[MinLengthValidator(1)]) description = models.TextField(blank=True, null=True) categories = models.ManyToManyField(SupermarketCategory, through='SupermarketCategoryRelation') @@ -177,18 +190,21 @@ class Supermarket(models.Model): return self.name -class SupermarketCategoryRelation(models.Model): +class SupermarketCategoryRelation(models.Model, PermissionModelMixin): supermarket = models.ForeignKey(Supermarket, on_delete=models.CASCADE, related_name='category_to_supermarket') category = models.ForeignKey(SupermarketCategory, on_delete=models.CASCADE, related_name='category_to_supermarket') order = models.IntegerField(default=0) objects = ScopedManager(space='supermarket__space') + def get_space(self): + return self.supermarket.space + class Meta: ordering = ('order',) -class SyncLog(models.Model): +class SyncLog(models.Model, PermissionModelMixin): sync = models.ForeignKey(Sync, on_delete=models.CASCADE) status = models.CharField(max_length=32) msg = models.TextField(default="") @@ -201,7 +217,7 @@ class SyncLog(models.Model): return f"{self.created_at}:{self.sync} - {self.status}" -class Keyword(models.Model): +class Keyword(models.Model, PermissionModelMixin): name = models.CharField(max_length=64, unique=True) icon = models.CharField(max_length=16, blank=True, null=True) description = models.TextField(default="", blank=True) @@ -218,7 +234,7 @@ class Keyword(models.Model): return f"{self.name}" -class Unit(models.Model): +class Unit(models.Model, PermissionModelMixin): name = models.CharField(unique=True, max_length=128, validators=[MinLengthValidator(1)]) description = models.TextField(blank=True, null=True) @@ -229,7 +245,7 @@ class Unit(models.Model): return self.name -class Food(models.Model): +class Food(models.Model, PermissionModelMixin): name = models.CharField(unique=True, max_length=128, validators=[MinLengthValidator(1)]) recipe = models.ForeignKey('Recipe', null=True, blank=True, on_delete=models.SET_NULL) supermarket_category = models.ForeignKey(SupermarketCategory, null=True, blank=True, on_delete=models.SET_NULL) @@ -243,13 +259,9 @@ class Food(models.Model): return self.name -class Ingredient(models.Model): - food = models.ForeignKey( - Food, on_delete=models.PROTECT, null=True, blank=True - ) - unit = models.ForeignKey( - Unit, on_delete=models.PROTECT, null=True, blank=True - ) +class Ingredient(models.Model, PermissionModelMixin): + food = models.ForeignKey(Food, on_delete=models.PROTECT, null=True, blank=True) + unit = models.ForeignKey(Unit, on_delete=models.PROTECT, null=True, blank=True) amount = models.DecimalField(default=0, decimal_places=16, max_digits=32) note = models.CharField(max_length=256, null=True, blank=True) is_header = models.BooleanField(default=False) @@ -258,6 +270,9 @@ class Ingredient(models.Model): objects = ScopedManager(space='step__recipe__space') + def get_space(self): + return self.step_set.first().recipe_set.first().space + def __str__(self): return str(self.amount) + ' ' + str(self.unit) + ' ' + str(self.food) @@ -265,7 +280,7 @@ class Ingredient(models.Model): ordering = ['order', 'pk'] -class Step(models.Model): +class Step(models.Model, PermissionModelMixin): TEXT = 'TEXT' TIME = 'TIME' @@ -283,6 +298,9 @@ class Step(models.Model): objects = ScopedManager(space='recipe__space') + def get_space(self): + return self.recipe_set.first().space + def get_instruction_render(self): from cookbook.helper.template_helper import render_instructions return render_instructions(self) @@ -291,7 +309,7 @@ class Step(models.Model): ordering = ['order', 'pk'] -class NutritionInformation(models.Model): +class NutritionInformation(models.Model, PermissionModelMixin): fats = models.DecimalField(default=0, decimal_places=16, max_digits=32) carbohydrates = models.DecimalField( default=0, decimal_places=16, max_digits=32 @@ -304,11 +322,14 @@ class NutritionInformation(models.Model): objects = ScopedManager(space='recipe__space') + def get_space(self): + return self.recipe_set.first().space + def __str__(self): return 'Nutrition' -class Recipe(models.Model): +class Recipe(models.Model, PermissionModelMixin): name = models.CharField(max_length=128) description = models.CharField(max_length=512, blank=True, null=True) servings = models.IntegerField(default=1) @@ -340,7 +361,7 @@ class Recipe(models.Model): return self.name -class Comment(models.Model): +class Comment(models.Model, PermissionModelMixin): recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE) text = models.TextField() created_by = models.ForeignKey(User, on_delete=models.CASCADE) @@ -349,11 +370,14 @@ class Comment(models.Model): objects = ScopedManager(space='recipe__space') + def get_space(self): + return self.recipe.space + def __str__(self): return self.text -class RecipeImport(models.Model): +class RecipeImport(models.Model, PermissionModelMixin): name = models.CharField(max_length=128) storage = models.ForeignKey(Storage, on_delete=models.PROTECT) file_uid = models.CharField(max_length=256, default="") @@ -367,13 +391,11 @@ class RecipeImport(models.Model): return self.name -class RecipeBook(models.Model): +class RecipeBook(models.Model, PermissionModelMixin): name = models.CharField(max_length=128) description = models.TextField(blank=True) icon = models.CharField(max_length=16, blank=True, null=True) - shared = models.ManyToManyField( - User, blank=True, related_name='shared_with' - ) + shared = models.ManyToManyField(User, blank=True, related_name='shared_with') created_by = models.ForeignKey(User, on_delete=models.CASCADE) space = models.ForeignKey(Space, on_delete=models.CASCADE) @@ -383,12 +405,15 @@ class RecipeBook(models.Model): return self.name -class RecipeBookEntry(models.Model): +class RecipeBookEntry(models.Model, PermissionModelMixin): recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE) book = models.ForeignKey(RecipeBook, on_delete=models.CASCADE) objects = ScopedManager(space='book__space') + def get_space(self): + return self.book.space + def __str__(self): return self.recipe.name @@ -402,7 +427,7 @@ class RecipeBookEntry(models.Model): unique_together = (('recipe', 'book'),) -class MealType(models.Model): +class MealType(models.Model, PermissionModelMixin): name = models.CharField(max_length=128) order = models.IntegerField(default=0) created_by = models.ForeignKey(User, on_delete=models.CASCADE) @@ -414,7 +439,7 @@ class MealType(models.Model): return self.name -class MealPlan(models.Model): +class MealPlan(models.Model, PermissionModelMixin): recipe = models.ForeignKey( Recipe, on_delete=models.CASCADE, blank=True, null=True ) @@ -443,10 +468,8 @@ class MealPlan(models.Model): return f'{self.get_label()} - {self.date} - {self.meal_type.name}' -class ShoppingListRecipe(models.Model): - recipe = models.ForeignKey( - Recipe, on_delete=models.CASCADE, null=True, blank=True - ) +class ShoppingListRecipe(models.Model, PermissionModelMixin): + recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, null=True, blank=True) servings = models.DecimalField(default=1, max_digits=8, decimal_places=4) space = models.ForeignKey(Space, on_delete=models.CASCADE) @@ -462,7 +485,7 @@ class ShoppingListRecipe(models.Model): return None -class ShoppingListEntry(models.Model): +class ShoppingListEntry(models.Model, PermissionModelMixin): list_recipe = models.ForeignKey(ShoppingListRecipe, on_delete=models.CASCADE, null=True, blank=True) food = models.ForeignKey(Food, on_delete=models.CASCADE) unit = models.ForeignKey(Unit, on_delete=models.CASCADE, null=True, blank=True) @@ -483,7 +506,7 @@ class ShoppingListEntry(models.Model): return None -class ShoppingList(models.Model): +class ShoppingList(models.Model, PermissionModelMixin): uuid = models.UUIDField(default=uuid.uuid4) note = models.TextField(blank=True, null=True) recipes = models.ManyToManyField(ShoppingListRecipe, blank=True) @@ -501,7 +524,7 @@ class ShoppingList(models.Model): return f'Shopping list {self.id}' -class ShareLink(models.Model): +class ShareLink(models.Model, PermissionModelMixin): recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE) uuid = models.UUIDField(default=uuid.uuid4) created_by = models.ForeignKey(User, on_delete=models.CASCADE) @@ -518,7 +541,7 @@ def default_valid_until(): return date.today() + timedelta(days=14) -class InviteLink(models.Model): +class InviteLink(models.Model, PermissionModelMixin): uuid = models.UUIDField(default=uuid.uuid4) username = models.CharField(blank=True, max_length=64) group = models.ForeignKey(Group, on_delete=models.CASCADE) @@ -536,7 +559,7 @@ class InviteLink(models.Model): return f'{self.uuid}' -class CookLog(models.Model): +class CookLog(models.Model, PermissionModelMixin): recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE) created_by = models.ForeignKey(User, on_delete=models.CASCADE) created_at = models.DateTimeField(default=timezone.now) @@ -550,7 +573,7 @@ class CookLog(models.Model): return self.recipe.name -class ViewLog(models.Model): +class ViewLog(models.Model, PermissionModelMixin): recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE) created_by = models.ForeignKey(User, on_delete=models.CASCADE) created_at = models.DateTimeField(auto_now_add=True) diff --git a/cookbook/views/edit.py b/cookbook/views/edit.py index 4259fc97..e1913a9a 100644 --- a/cookbook/views/edit.py +++ b/cookbook/views/edit.py @@ -3,9 +3,10 @@ import os from django.contrib import messages from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, render -from django.urls import reverse +from django.urls import reverse, reverse_lazy from django.utils.translation import gettext as _ from django.views.generic import UpdateView +from django_scopes import scopes_disabled from cookbook.forms import (CommentForm, ExternalRecipeForm, FoodForm, FoodMergeForm, KeywordForm, MealPlanForm, @@ -24,7 +25,7 @@ from cookbook.provider.nextcloud import Nextcloud @group_required('guest') def switch_recipe(request, pk): - recipe = get_object_or_404(Recipe, pk=pk) + recipe = get_object_or_404(Recipe, pk=pk, space=request.space) if recipe.internal: return HttpResponseRedirect(reverse('edit_internal_recipe', args=[pk])) else: @@ -33,7 +34,7 @@ def switch_recipe(request, pk): @group_required('user') def convert_recipe(request, pk): - recipe = get_object_or_404(Recipe, pk=pk) + recipe = get_object_or_404(Recipe, pk=pk, space=request.space) if not recipe.internal: recipe.internal = True recipe.save() @@ -43,7 +44,7 @@ def convert_recipe(request, pk): @group_required('user') def internal_recipe_update(request, pk): - recipe_instance = get_object_or_404(Recipe, pk=pk) + recipe_instance = get_object_or_404(Recipe, pk=pk, space=request.space) return render( request, 'forms/edit_internal_recipe.html', {'recipe': recipe_instance} @@ -62,7 +63,7 @@ class SyncUpdate(GroupRequiredMixin, UpdateView): return reverse('edit_sync', kwargs={'pk': self.object.pk}) def get_context_data(self, **kwargs): - context = super(SyncUpdate, self).get_context_data(**kwargs) + context = super().get_context_data(**kwargs) context['title'] = _("Sync") return context @@ -79,7 +80,7 @@ class KeywordUpdate(GroupRequiredMixin, UpdateView): return reverse('edit_keyword', kwargs={'pk': self.object.pk}) def get_context_data(self, **kwargs): - context = super(KeywordUpdate, self).get_context_data(**kwargs) + context = super().get_context_data(**kwargs) context['title'] = _("Keyword") return context @@ -103,12 +104,10 @@ class FoodUpdate(GroupRequiredMixin, UpdateView): @group_required('admin') def edit_storage(request, pk): - instance = get_object_or_404(Storage, pk=pk) + instance = get_object_or_404(Storage, pk=pk, space=request.space) if not (instance.created_by == request.user or request.user.is_superuser): - messages.add_message( - request, messages.ERROR, _('You cannot edit this storage!') - ) + messages.add_message(request, messages.ERROR, _('You cannot edit this storage!')) return HttpResponseRedirect(reverse('list_storage')) if request.method == "POST": @@ -225,7 +224,7 @@ class ExternalRecipeUpdate(GroupRequiredMixin, UpdateView): def form_valid(self, form): self.object = form.save(commit=False) - old_recipe = Recipe.objects.get(pk=self.object.pk) + old_recipe = Recipe.objects.get(pk=self.object.pk, space=self.request.space) if not old_recipe.name == self.object.name: if self.object.storage.method == Storage.DROPBOX: # TODO central location to handle storage type switches @@ -277,45 +276,32 @@ def edit_ingredients(request): new_unit = units_form.cleaned_data['new_unit'] old_unit = units_form.cleaned_data['old_unit'] if new_unit != old_unit: - recipe_ingredients = Ingredient.objects \ - .filter(unit=old_unit).all() + recipe_ingredients = Ingredient.objects.filter(unit=old_unit, space=request.space).all() for i in recipe_ingredients: i.unit = new_unit i.save() old_unit.delete() success = True - messages.add_message( - request, messages.SUCCESS, _('Units merged!') - ) + messages.add_message(request, messages.SUCCESS, _('Units merged!')) else: - messages.add_message( - request, - messages.ERROR, - _('Cannot merge with the same object!') - ) + messages.add_message(request, messages.ERROR, _('Cannot merge with the same object!')) food_form = FoodMergeForm(request.POST, prefix=FoodMergeForm.prefix) if food_form.is_valid(): new_food = food_form.cleaned_data['new_food'] old_food = food_form.cleaned_data['old_food'] if new_food != old_food: - ingredients = Ingredient.objects.filter(food=old_food).all() + ingredients = Ingredient.objects.filter(food=old_food, space=request.space).all() for i in ingredients: i.food = new_food i.save() old_food.delete() success = True - messages.add_message( - request, messages.SUCCESS, _('Foods merged!') - ) + messages.add_message(request, messages.SUCCESS, _('Foods merged!')) else: - messages.add_message( - request, - messages.ERROR, - _('Cannot merge with the same object!') - ) + messages.add_message(request, messages.ERROR, _('Cannot merge with the same object!')) if success: units_form = UnitMergeForm() @@ -324,8 +310,4 @@ def edit_ingredients(request): units_form = UnitMergeForm() food_form = FoodMergeForm() - return render( - request, - 'forms/ingredients.html', - {'units_form': units_form, 'food_form': food_form} - ) + return render(request, 'forms/ingredients.html', {'units_form': units_form, 'food_form': food_form}) diff --git a/cookbook/views/import_export.py b/cookbook/views/import_export.py index a77113d5..d7ea9de0 100644 --- a/cookbook/views/import_export.py +++ b/cookbook/views/import_export.py @@ -62,7 +62,7 @@ def export_recipe(request): recipe = request.GET.get('r') if recipe: if re.match(r'^([0-9])+$', recipe): - if recipe := Recipe.objects.filter(pk=int(recipe)).first(): + if recipe := Recipe.objects.filter(pk=int(recipe), space=request.space).first(): form = ExportForm(initial={'recipes': recipe}, user=request.user) return render(request, 'export.html', {'form': form})