diff --git a/cookbook/filters.py b/cookbook/filters.py index c1cc86463..2d75bde35 100644 --- a/cookbook/filters.py +++ b/cookbook/filters.py @@ -3,77 +3,78 @@ from django.conf import settings from django.contrib.postgres.search import TrigramSimilarity from django.db.models import Q from django.utils.translation import gettext as _ +from django_scopes import scopes_disabled from cookbook.forms import MultiSelectWidget from cookbook.models import Food, Keyword, Recipe, ShoppingList +with scopes_disabled(): + class RecipeFilter(django_filters.FilterSet): + name = django_filters.CharFilter(method='filter_name') + keywords = django_filters.ModelMultipleChoiceFilter( + queryset=Keyword.objects.all(), + widget=MultiSelectWidget, + method='filter_keywords' + ) + foods = django_filters.ModelMultipleChoiceFilter( + queryset=Food.objects.all(), + widget=MultiSelectWidget, + method='filter_foods', + label=_('Ingredients') + ) -class RecipeFilter(django_filters.FilterSet): - name = django_filters.CharFilter(method='filter_name') - keywords = django_filters.ModelMultipleChoiceFilter( - queryset=Keyword.objects.all(), - widget=MultiSelectWidget, - method='filter_keywords' - ) - foods = django_filters.ModelMultipleChoiceFilter( - queryset=Food.objects.all(), - widget=MultiSelectWidget, - method='filter_foods', - label=_('Ingredients') - ) - - @staticmethod - def filter_keywords(queryset, name, value): - if not name == 'keywords': + @staticmethod + def filter_keywords(queryset, name, value): + if not name == 'keywords': + return queryset + for x in value: + queryset = queryset.filter(keywords=x) return queryset - for x in value: - queryset = queryset.filter(keywords=x) - return queryset - @staticmethod - def filter_foods(queryset, name, value): - if not name == 'foods': + @staticmethod + def filter_foods(queryset, name, value): + if not name == 'foods': + return queryset + for x in value: + queryset = queryset.filter( + steps__ingredients__food__name=x + ).distinct() return queryset - for x in value: - queryset = queryset.filter( - steps__ingredients__food__name=x - ).distinct() - return queryset - @staticmethod - def filter_name(queryset, name, value): - if not name == 'name': + @staticmethod + def filter_name(queryset, name, value): + if not name == 'name': + return queryset + if settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql_psycopg2': # noqa: E501 + queryset = queryset \ + .annotate(similarity=TrigramSimilarity('name', value), ) \ + .filter(Q(similarity__gt=0.1) | Q(name__unaccent__icontains=value)) \ + .order_by('-similarity') + else: + queryset = queryset.filter(name__icontains=value) return queryset - if settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql_psycopg2': # noqa: E501 - queryset = queryset \ - .annotate(similarity=TrigramSimilarity('name', value), ) \ - .filter(Q(similarity__gt=0.1) | Q(name__unaccent__icontains=value)) \ - .order_by('-similarity') - else: - queryset = queryset.filter(name__icontains=value) - return queryset - class Meta: - model = Recipe - fields = ['name', 'keywords', 'foods', 'internal'] + class Meta: + model = Recipe + fields = ['name', 'keywords', 'foods', 'internal'] -class IngredientFilter(django_filters.FilterSet): - name = django_filters.CharFilter(lookup_expr='icontains') + class IngredientFilter(django_filters.FilterSet): + name = django_filters.CharFilter(lookup_expr='icontains') - class Meta: - model = Food - fields = ['name'] + class Meta: + model = Food + fields = ['name'] -class ShoppingListFilter(django_filters.FilterSet): + class ShoppingListFilter(django_filters.FilterSet): - def __init__(self, data=None, *args, **kwargs): - if data is not None: - data = data.copy() - data.setdefault("finished", False) - super(ShoppingListFilter, self).__init__(data, *args, **kwargs) + def __init__(self, data=None, *args, **kwargs): + if data is not None: + data = data.copy() + data.setdefault("finished", False) + super(ShoppingListFilter, self).__init__(data, *args, **kwargs) - class Meta: - model = ShoppingList - fields = ['finished'] + class Meta: + model = ShoppingList + fields = ['finished'] diff --git a/cookbook/forms.py b/cookbook/forms.py index 1d6d0d79e..6d8d5dba4 100644 --- a/cookbook/forms.py +++ b/cookbook/forms.py @@ -1,6 +1,7 @@ from django import forms from django.forms import widgets from django.utils.translation import gettext_lazy as _ +from django_scopes.forms import SafeModelChoiceField, SafeModelMultipleChoiceField from emoji_picker.widgets import EmojiPickerTextInput from .models import (Comment, Food, InviteLink, Keyword, MealPlan, Recipe, @@ -74,18 +75,13 @@ class UserNameForm(forms.ModelForm): class ExternalRecipeForm(forms.ModelForm): file_path = forms.CharField(disabled=True, required=False) - storage = forms.ModelChoiceField( - queryset=Storage.objects.all(), - disabled=True, - required=False - ) file_uid = forms.CharField(disabled=True, required=False) class Meta: model = Recipe fields = ( - 'name', 'keywords', 'description', 'servings', 'working_time', 'waiting_time', - 'file_path', 'storage', 'file_uid' + 'name', 'description', 'servings', 'working_time', 'waiting_time', + 'file_path', 'file_uid' ) labels = { @@ -96,39 +92,7 @@ class ExternalRecipeForm(forms.ModelForm): 'file_path': _('Path'), 'file_uid': _('Storage UID'), } - widgets = {'keywords': MultiSelectWidget} - - -class InternalRecipeForm(forms.ModelForm): - ingredients = forms.CharField(widget=forms.HiddenInput(), required=False) - - class Meta: - model = Recipe - fields = ( - 'name', 'image', 'working_time', - 'waiting_time', 'servings', 'keywords' - ) - - labels = { - 'name': _('Name'), - 'keywords': _('Keywords'), - 'working_time': _('Preparation time in minutes'), - 'waiting_time': _('Waiting time (cooking/baking) in minutes'), - 'servings': _('Number of servings'), - } - widgets = {'keywords': MultiSelectWidget} - - -class ShoppingForm(forms.Form): - recipe = forms.ModelMultipleChoiceField( - queryset=Recipe.objects.filter(internal=True).all(), - widget=MultiSelectWidget - ) - markdown_format = forms.BooleanField( - help_text=_('Include - [ ] in list for easier usage in markdown based documents.'), # noqa: E501 - required=False, - initial=False - ) + # widgets = {'keywords': MultiSelectWidget} class ImportExportBase(forms.Form): @@ -150,37 +114,44 @@ class ImportForm(ImportExportBase): class ExportForm(ImportExportBase): - recipes = forms.ModelMultipleChoiceField(queryset=Recipe.objects.filter(internal=True).all(), widget=MultiSelectWidget) + recipes = forms.ModelMultipleChoiceField(widget=MultiSelectWidget, queryset=None) + + def __init__(self, *args, **kwargs): + super().__init__() + user = kwargs.pop('user') + self.fields['recipes'].queryset = Recipe.objects.filter(internal=True).filter(space=user.userpreference.space).all() class UnitMergeForm(forms.Form): prefix = 'unit' - new_unit = forms.ModelChoiceField( - queryset=Unit.objects.all(), + new_unit = SafeModelChoiceField( + queryset=Unit.objects.none(), widget=SelectWidget, label=_('New Unit'), help_text=_('New unit that other gets replaced by.'), ) - old_unit = forms.ModelChoiceField( - queryset=Unit.objects.all(), + old_unit = SafeModelChoiceField( + queryset=Unit.objects.none(), widget=SelectWidget, label=_('Old Unit'), help_text=_('Unit that should be replaced.'), ) +# todo spaces form here on + class FoodMergeForm(forms.Form): prefix = 'food' - new_food = forms.ModelChoiceField( - queryset=Food.objects.all(), + new_food = SafeModelChoiceField( + queryset=Food.objects.none(), widget=SelectWidget, label=_('New Food'), help_text=_('New food that other gets replaced by.'), ) - old_food = forms.ModelChoiceField( - queryset=Food.objects.all(), + old_food = SafeModelChoiceField( + queryset=Food.objects.none(), widget=SelectWidget, label=_('Old Food'), help_text=_('Food that should be replaced.'), @@ -215,6 +186,11 @@ class FoodForm(forms.ModelForm): fields = ('name', 'description', 'ignore_shopping', 'recipe', 'supermarket_category') widgets = {'recipe': SelectWidget} + field_classes = { + 'recipe': SafeModelChoiceField, + 'supermarket_category': SafeModelChoiceField, + } + class StorageForm(forms.ModelForm): username = forms.CharField( @@ -252,17 +228,25 @@ class RecipeBookEntryForm(forms.ModelForm): model = RecipeBookEntry fields = ('book',) + field_classes = { + 'book': SafeModelChoiceField, + } + class SyncForm(forms.ModelForm): class Meta: model = Sync fields = ('storage', 'path', 'active') + field_classes = { + 'storage': SafeModelChoiceField, + } + class BatchEditForm(forms.Form): search = forms.CharField(label=_('Search String')) keywords = forms.ModelMultipleChoiceField( - queryset=Keyword.objects.all().order_by('id'), + queryset=Keyword.objects.none().order_by('id'), required=False, widget=MultiSelectWidget ) @@ -280,6 +264,9 @@ class ImportRecipeForm(forms.ModelForm): 'file_uid': _('File ID'), } widgets = {'keywords': MultiSelectWidget} + field_classes = { + 'keywords': SafeModelChoiceField, + } class RecipeBookForm(forms.ModelForm): @@ -287,6 +274,9 @@ class RecipeBookForm(forms.ModelForm): model = RecipeBook fields = ('name', 'icon', 'description', 'shared') widgets = {'icon': EmojiPickerTextInput, 'shared': MultiSelectWidget} + field_classes = { + 'shared': SafeModelMultipleChoiceField, + } class MealPlanForm(forms.ModelForm): @@ -318,6 +308,11 @@ class MealPlanForm(forms.ModelForm): 'date': DateWidget, 'shared': MultiSelectWidget } + field_classes = { + 'recipe': SafeModelChoiceField, + 'meal_type': SafeModelChoiceField, + 'shared': SafeModelMultipleChoiceField, + } class InviteLinkForm(forms.ModelForm): diff --git a/cookbook/models.py b/cookbook/models.py index 81eb6ec07..bedc619ae 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -10,6 +10,7 @@ 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, STICKY_NAV_PREF_DEFAULT) @@ -107,6 +108,9 @@ class UserPreference(models.Model): shopping_auto_sync = models.IntegerField(default=5) sticky_navbar = models.BooleanField(default=STICKY_NAV_PREF_DEFAULT) + space = models.ForeignKey(Space, blank=True, on_delete=models.CASCADE) + objects = ScopedManager(space='space') + def __str__(self): return str(self.user) @@ -128,6 +132,9 @@ class Storage(models.Model): path = models.CharField(blank=True, default='', max_length=256) created_by = models.ForeignKey(User, on_delete=models.PROTECT) + space = models.ForeignKey(Space, blank=True, on_delete=models.CASCADE) + objects = ScopedManager(space='space') + def __str__(self): return self.name @@ -140,6 +147,9 @@ class Sync(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + space = models.ForeignKey(Space, blank=True, on_delete=models.CASCADE) + objects = ScopedManager(space='space') + def __str__(self): return self.path @@ -148,6 +158,9 @@ class SupermarketCategory(models.Model): name = models.CharField(unique=True, max_length=128, validators=[MinLengthValidator(1)]) description = models.TextField(blank=True, null=True) + space = models.ForeignKey(Space, blank=True, on_delete=models.CASCADE) + objects = ScopedManager(space='space') + def __str__(self): return self.name @@ -157,6 +170,9 @@ class Supermarket(models.Model): description = models.TextField(blank=True, null=True) categories = models.ManyToManyField(SupermarketCategory, through='SupermarketCategoryRelation') + space = models.ForeignKey(Space, blank=True, on_delete=models.CASCADE) + objects = ScopedManager(space='space') + def __str__(self): return self.name @@ -166,6 +182,8 @@ class SupermarketCategoryRelation(models.Model): category = models.ForeignKey(SupermarketCategory, on_delete=models.CASCADE, related_name='category_to_supermarket') order = models.IntegerField(default=0) + objects = ScopedManager(space='supermarket__space') + class Meta: ordering = ('order',) @@ -176,6 +194,9 @@ class SyncLog(models.Model): msg = models.TextField(default="") created_at = models.DateTimeField(auto_now_add=True) + space = models.ForeignKey(Space, blank=True, on_delete=models.CASCADE) + objects = ScopedManager(space='space') + def __str__(self): return f"{self.created_at}:{self.sync} - {self.status}" @@ -187,6 +208,9 @@ class Keyword(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + space = models.ForeignKey(Space, blank=True, on_delete=models.CASCADE) + objects = ScopedManager(space='space') + def __str__(self): if self.icon: return f"{self.icon} {self.name}" @@ -198,6 +222,9 @@ class Unit(models.Model): name = models.CharField(unique=True, max_length=128, validators=[MinLengthValidator(1)]) description = models.TextField(blank=True, null=True) + space = models.ForeignKey(Space, blank=True, on_delete=models.CASCADE) + objects = ScopedManager(space='space') + def __str__(self): return self.name @@ -209,6 +236,9 @@ class Food(models.Model): ignore_shopping = models.BooleanField(default=False) description = models.TextField(default='', blank=True) + space = models.ForeignKey(Space, blank=True, on_delete=models.CASCADE) + objects = ScopedManager(space='space') + def __str__(self): return self.name @@ -226,6 +256,8 @@ class Ingredient(models.Model): no_amount = models.BooleanField(default=False) order = models.IntegerField(default=0) + objects = ScopedManager(space='step__recipe__space') + def __str__(self): return str(self.amount) + ' ' + str(self.unit) + ' ' + str(self.food) @@ -249,6 +281,8 @@ class Step(models.Model): order = models.IntegerField(default=0) show_as_header = models.BooleanField(default=True) + objects = ScopedManager(space='recipe__space') + def get_instruction_render(self): from cookbook.helper.template_helper import render_instructions return render_instructions(self) @@ -268,6 +302,8 @@ class NutritionInformation(models.Model): max_length=512, default="", null=True, blank=True ) + objects = ScopedManager(space='recipe__space') + def __str__(self): return 'Nutrition' @@ -297,7 +333,8 @@ class Recipe(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) - objects = RandomManager() + space = models.ForeignKey(Space, blank=True, on_delete=models.CASCADE) + objects = ScopedManager(space='space') def __str__(self): return self.name @@ -310,6 +347,8 @@ class Comment(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + objects = ScopedManager(space='recipe__space') + def __str__(self): return self.text @@ -321,6 +360,9 @@ class RecipeImport(models.Model): file_path = models.CharField(max_length=512, default="") created_at = models.DateTimeField(auto_now_add=True) + space = models.ForeignKey(Space, blank=True, on_delete=models.CASCADE) + objects = ScopedManager(space='space') + def __str__(self): return self.name @@ -334,6 +376,9 @@ class RecipeBook(models.Model): ) created_by = models.ForeignKey(User, on_delete=models.CASCADE) + space = models.ForeignKey(Space, blank=True, on_delete=models.CASCADE) + objects = ScopedManager(space='space') + def __str__(self): return self.name @@ -342,6 +387,9 @@ class RecipeBookEntry(models.Model): recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE) book = models.ForeignKey(RecipeBook, on_delete=models.CASCADE) + space = models.ForeignKey(Space, blank=True, on_delete=models.CASCADE) + objects = ScopedManager(space='space') + def __str__(self): return self.recipe.name @@ -360,6 +408,9 @@ class MealType(models.Model): order = models.IntegerField(default=0) created_by = models.ForeignKey(User, on_delete=models.CASCADE) + space = models.ForeignKey(Space, blank=True, on_delete=models.CASCADE) + objects = ScopedManager(space='space') + def __str__(self): return self.name @@ -378,6 +429,9 @@ class MealPlan(models.Model): note = models.TextField(blank=True) date = models.DateField() + space = models.ForeignKey(Space, blank=True, on_delete=models.CASCADE) + objects = ScopedManager(space='space') + def get_label(self): if self.title: return self.title @@ -396,6 +450,9 @@ class ShoppingListRecipe(models.Model): ) servings = models.DecimalField(default=1, max_digits=8, decimal_places=4) + space = models.ForeignKey(Space, blank=True, on_delete=models.CASCADE) + objects = ScopedManager(space='space') + def __str__(self): return f'Shopping list recipe {self.id} - {self.recipe}' @@ -414,6 +471,9 @@ class ShoppingListEntry(models.Model): order = models.IntegerField(default=0) checked = models.BooleanField(default=False) + space = models.ForeignKey(Space, blank=True, on_delete=models.CASCADE) + objects = ScopedManager(space='space') + def __str__(self): return f'Shopping list entry {self.id}' @@ -435,6 +495,9 @@ class ShoppingList(models.Model): created_by = models.ForeignKey(User, on_delete=models.CASCADE) created_at = models.DateTimeField(auto_now_add=True) + space = models.ForeignKey(Space, blank=True, on_delete=models.CASCADE) + objects = ScopedManager(space='space') + def __str__(self): return f'Shopping list {self.id}' @@ -445,6 +508,9 @@ class ShareLink(models.Model): created_by = models.ForeignKey(User, on_delete=models.CASCADE) created_at = models.DateTimeField(auto_now_add=True) + space = models.ForeignKey(Space, blank=True, on_delete=models.CASCADE) + objects = ScopedManager(space='space') + def __str__(self): return f'{self.recipe} - {self.uuid}' @@ -464,6 +530,9 @@ class InviteLink(models.Model): created_by = models.ForeignKey(User, on_delete=models.CASCADE) created_at = models.DateTimeField(auto_now_add=True) + space = models.ForeignKey(Space, blank=True, on_delete=models.CASCADE) + objects = ScopedManager(space='space') + def __str__(self): return f'{self.uuid}' @@ -475,6 +544,9 @@ class CookLog(models.Model): rating = models.IntegerField(null=True) servings = models.IntegerField(default=0) + space = models.ForeignKey(Space, blank=True, on_delete=models.CASCADE) + objects = ScopedManager(space='space') + def __str__(self): return self.recipe.name @@ -484,5 +556,8 @@ class ViewLog(models.Model): created_by = models.ForeignKey(User, on_delete=models.CASCADE) created_at = models.DateTimeField(auto_now_add=True) + space = models.ForeignKey(Space, blank=True, on_delete=models.CASCADE) + objects = ScopedManager(space='space') + def __str__(self): return self.recipe.name diff --git a/cookbook/urls.py b/cookbook/urls.py index ba9a8d75e..08b3a834f 100644 --- a/cookbook/urls.py +++ b/cookbook/urls.py @@ -89,7 +89,6 @@ urlpatterns = [ path('api/log_cooking//', api.log_cooking, name='api_log_cooking'), path('api/plan-ical///', api.get_plan_ical, name='api_get_plan_ical'), path('api/recipe-from-url/', api.recipe_from_url, name='api_recipe_from_url'), - path('api/backup/', api.get_backup, name='api_backup'), path('api/ingredient-from-string/', api.ingredient_from_string, name='api_ingredient_from_string'), path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'), diff --git a/cookbook/views/api.py b/cookbook/views/api.py index ab480f31b..eb111b50f 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -59,7 +59,7 @@ from recipes.settings import DEMO class StandardFilterMixin(ViewSetMixin): def get_queryset(self): - queryset = self.queryset + queryset = self.queryset.filter(userpreference__space=self.request.user.userpreference.space) query = self.request.query_params.get('query', None) if query is not None: queryset = queryset.filter(name__icontains=query) @@ -90,13 +90,13 @@ class UserNameViewSet(viewsets.ReadOnlyModelViewSet): - **filter_list**: array of user id's to get names for """ - queryset = User.objects.all() + queryset = User.objects serializer_class = UserNameSerializer permission_classes = [CustomIsGuest] http_method_names = ['get'] def get_queryset(self): - queryset = self.queryset + queryset = self.queryset.filter(userpreference__space=self.request.user.userpreference.space) try: filter_list = self.request.query_params.get('filter_list', None) if filter_list is not None: @@ -110,7 +110,7 @@ class UserNameViewSet(viewsets.ReadOnlyModelViewSet): class UserPreferenceViewSet(viewsets.ModelViewSet): - queryset = UserPreference.objects.all() + queryset = UserPreference.objects serializer_class = UserPreferenceSerializer permission_classes = [CustomIsOwner, ] @@ -120,35 +120,45 @@ class UserPreferenceViewSet(viewsets.ModelViewSet): serializer.save(user=self.request.user) def get_queryset(self): - if self.request.user.is_superuser: - return self.queryset return self.queryset.filter(user=self.request.user) class StorageViewSet(viewsets.ModelViewSet): # TODO handle delete protect error and adjust test - queryset = Storage.objects.all() + queryset = Storage.objects serializer_class = StorageSerializer permission_classes = [CustomIsAdmin, ] + def get_queryset(self): + return self.queryset.filter(space=self.request.user.userpreference.space) + class SyncViewSet(viewsets.ModelViewSet): - queryset = Sync.objects.all() + queryset = Sync.objects serializer_class = SyncSerializer permission_classes = [CustomIsAdmin, ] + def get_queryset(self): + return self.queryset.filter(space=self.request.user.userpreference.space) + class SyncLogViewSet(viewsets.ReadOnlyModelViewSet): - queryset = SyncLog.objects.all() + queryset = SyncLog.objects serializer_class = SyncLogSerializer permission_classes = [CustomIsAdmin, ] + def get_queryset(self): + return self.queryset.filter(space=self.request.user.userpreference.space) + class SupermarketViewSet(viewsets.ModelViewSet, StandardFilterMixin): - queryset = Supermarket.objects.all() + queryset = Supermarket.objects serializer_class = SupermarketSerializer permission_classes = [CustomIsUser] + def get_queryset(self): + return self.queryset.filter(space=self.request.user.userpreference.space) + class KeywordViewSet(viewsets.ModelViewSet, StandardFilterMixin): """ @@ -159,44 +169,48 @@ class KeywordViewSet(viewsets.ModelViewSet, StandardFilterMixin): in the keyword name (case in-sensitive) - **limit**: limits the amount of returned results """ - queryset = Keyword.objects.all() + queryset = Keyword.objects serializer_class = KeywordSerializer permission_classes = [CustomIsUser] + def get_queryset(self): + return self.queryset.filter(space=self.request.user.userpreference.space) + class UnitViewSet(viewsets.ModelViewSet, StandardFilterMixin): - queryset = Unit.objects.all() + queryset = Unit.objects serializer_class = UnitSerializer permission_classes = [CustomIsUser] + def get_queryset(self): + return self.queryset.filter(space=self.request.user.userpreference.space) + class FoodViewSet(viewsets.ModelViewSet, StandardFilterMixin): - queryset = Food.objects.all() + queryset = Food.objects serializer_class = FoodSerializer permission_classes = [CustomIsUser] + def get_queryset(self): + return self.queryset.filter(space=self.request.user.userpreference.space) + class RecipeBookViewSet(viewsets.ModelViewSet, StandardFilterMixin): - queryset = RecipeBook.objects.all() + queryset = RecipeBook.objects serializer_class = RecipeBookSerializer permission_classes = [CustomIsOwner] def get_queryset(self): - self.queryset = super(RecipeBookViewSet, self).get_queryset() - if self.request.user.is_superuser: - return self.queryset - return self.queryset.filter(created_by=self.request.user) + return self.queryset.filter(created_by=self.request.user).filter(space=self.request.user.userpreference.space) class RecipeBookEntryViewSet(viewsets.ModelViewSet, viewsets.GenericViewSet): - queryset = RecipeBookEntry.objects.all() + queryset = RecipeBookEntry.objects serializer_class = RecipeBookEntrySerializer permission_classes = [CustomIsOwner] def get_queryset(self): - if self.request.user.is_superuser: - return self.queryset - return self.queryset.filter(created_by=self.request.user) + return self.queryset.filter(created_by=self.request.user).filter(space=self.request.user.userpreference.space) class MealPlanViewSet(viewsets.ModelViewSet): @@ -208,15 +222,15 @@ class MealPlanViewSet(viewsets.ModelViewSet): - **to_date**: filter upward to (inclusive) certain date """ - queryset = MealPlan.objects.all() + queryset = MealPlan.objects serializer_class = MealPlanSerializer permission_classes = [CustomIsOwner] def get_queryset(self): - queryset = MealPlan.objects.filter( + queryset = self.queryset.filter( Q(created_by=self.request.user) | Q(shared=self.request.user) - ).distinct().all() + ).filter(space=self.request.user.userpreference.space).distinct().all() from_date = self.request.query_params.get('from_date', None) if from_date is not None: @@ -233,26 +247,32 @@ class MealTypeViewSet(viewsets.ModelViewSet): returns list of meal types created by the requesting user ordered by the order field. """ - queryset = MealType.objects.order_by('order').all() + queryset = MealType.objects serializer_class = MealTypeSerializer permission_classes = [CustomIsOwner] def get_queryset(self): - queryset = MealType.objects.order_by('order', 'id').filter(created_by=self.request.user).all() + queryset = self.queryset.order_by('order', 'id').filter(created_by=self.request.user).filter(space=self.request.user.userpreference.space).all() return queryset class IngredientViewSet(viewsets.ModelViewSet): - queryset = Ingredient.objects.all() + queryset = Ingredient.objects serializer_class = IngredientSerializer permission_classes = [CustomIsUser] + def get_queryset(self): + return self.queryset.filter(step__recipe__space=self.request.user.userpreference.space) + class StepViewSet(viewsets.ModelViewSet): - queryset = Step.objects.all() + queryset = Step.objects serializer_class = StepSerializer permission_classes = [CustomIsUser] + def get_queryset(self): + return self.queryset.filter(recipe__space=self.request.user.userpreference.space) + class RecipeViewSet(viewsets.ModelViewSet, StandardFilterMixin): """ @@ -263,18 +283,19 @@ class RecipeViewSet(viewsets.ModelViewSet, StandardFilterMixin): in the recipe name (case in-sensitive) - **limit**: limits the amount of returned results """ - queryset = Recipe.objects.all() + queryset = Recipe.objects serializer_class = RecipeSerializer # TODO split read and write permission for meal plan guest permission_classes = [CustomIsShare | CustomIsGuest] def get_queryset(self): + queryset = self.queryset.filter(space=self.request.user.userpreference.space) internal = self.request.query_params.get('internal', None) if internal: - self.queryset = self.queryset.filter(internal=True) + queryset = queryset.filter(internal=True) - return super().get_queryset() + return queryset # TODO write extensive tests for permissions @@ -317,34 +338,32 @@ class RecipeViewSet(viewsets.ModelViewSet, StandardFilterMixin): class ShoppingListRecipeViewSet(viewsets.ModelViewSet): - queryset = ShoppingListRecipe.objects.all() + queryset = ShoppingListRecipe.objects serializer_class = ShoppingListRecipeSerializer permission_classes = [CustomIsOwner, ] def get_queryset(self): - return self.queryset.filter(shoppinglist__created_by=self.request.user).all() + return self.queryset.filter(shoppinglist__created_by=self.request.user).filter(space=self.request.user.userpreference.space).all() class ShoppingListEntryViewSet(viewsets.ModelViewSet): - queryset = ShoppingListEntry.objects.all() + queryset = ShoppingListEntry.objects serializer_class = ShoppingListEntrySerializer permission_classes = [CustomIsOwner, ] def get_queryset(self): - return self.queryset.filter(shoppinglist__created_by=self.request.user).all() + return self.queryset.filter(shoppinglist__created_by=self.request.user).filter(space=self.request.user.userpreference.space).all() class ShoppingListViewSet(viewsets.ModelViewSet): - queryset = ShoppingList.objects.all() + queryset = ShoppingList.objects serializer_class = ShoppingListSerializer permission_classes = [CustomIsOwner | CustomIsShared] def get_queryset(self): - if self.request.user.is_superuser: - return self.queryset return self.queryset.filter( Q(created_by=self.request.user) | Q(shared=self.request.user) - ).all() + ).filter(space=self.request.user.userpreference.space).all() def get_serializer_class(self): autosync = self.request.query_params.get('autosync', None) @@ -354,21 +373,21 @@ class ShoppingListViewSet(viewsets.ModelViewSet): class ViewLogViewSet(viewsets.ModelViewSet): - queryset = ViewLog.objects.all() + queryset = ViewLog.objects serializer_class = ViewLogSerializer permission_classes = [CustomIsOwner] def get_queryset(self): - return CookLog.objects.filter(created_by=self.request.user).all()[:5] + return CookLog.objects.filter(created_by=self.request.user).filter(space=self.request.user.userpreference.space).all()[:5] class CookLogViewSet(viewsets.ModelViewSet): - queryset = CookLog.objects.all() + queryset = CookLog.objects serializer_class = CookLogSerializer permission_classes = [CustomIsOwner] def get_queryset(self): - queryset = CookLog.objects.filter(created_by=self.request.user).all()[:5] + queryset = CookLog.objects.filter(created_by=self.request.user).filter(space=self.request.user.userpreference.space).all()[:5] return queryset @@ -395,7 +414,7 @@ def update_recipe_links(recipe): @group_required('user') def get_external_file_link(request, recipe_id): - recipe = Recipe.objects.get(id=recipe_id) + recipe = Recipe.objects.filter(space=request.user.userpreference.space).get(id=recipe_id) if not recipe.link: update_recipe_links(recipe) @@ -404,7 +423,7 @@ def get_external_file_link(request, recipe_id): @group_required('guest') def get_recipe_file(request, recipe_id): - recipe = Recipe.objects.get(id=recipe_id) + recipe = Recipe.objects.filter(space=request.user.userpreference.space).get(id=recipe_id) if recipe.storage: return FileResponse(get_recipe_provider(recipe).get_file(recipe)) else: @@ -419,7 +438,7 @@ def sync_all(request): ) return redirect('index') - monitors = Sync.objects.filter(active=True) + monitors = Sync.objects.filter(active=True).filter(space=request.user.userpreference.space) error = False for monitor in monitors: @@ -471,7 +490,7 @@ def log_cooking(request, recipe_id): def get_plan_ical(request, from_date, to_date): queryset = MealPlan.objects.filter( Q(created_by=request.user) | Q(shared=request.user) - ).distinct().all() + ).filter(space=request.user.userpreference.space).distinct().all() if from_date is not None: queryset = queryset.filter(date__gte=from_date) @@ -525,22 +544,6 @@ def recipe_from_url(request): return get_from_html(response.text, url) -@group_required('admin') -def get_backup(request): - if not request.user.is_superuser: - return HttpResponse('', status=403) - - buf = io.StringIO() - management.call_command( - 'dumpdata', exclude=['contenttypes', 'auth'], stdout=buf - ) - - response = FileResponse(buf.getvalue()) - response["Content-Disposition"] = f'attachment; filename=backup{date_format(timezone.now(), format="SHORT_DATETIME_FORMAT", use_l10n=True)}.json' # noqa: E501 - - return response - - @group_required('user') def ingredient_from_string(request): text = request.POST['text'] diff --git a/cookbook/views/import_export.py b/cookbook/views/import_export.py index 02361c15a..a77113d5e 100644 --- a/cookbook/views/import_export.py +++ b/cookbook/views/import_export.py @@ -49,7 +49,7 @@ def import_recipe(request): @group_required('user') def export_recipe(request): if request.method == "POST": - form = ExportForm(request.POST) + form = ExportForm(request.POST, user=request.user) if form.is_valid(): try: integration = get_integration(request, form.cleaned_data['type']) @@ -58,11 +58,11 @@ def export_recipe(request): messages.add_message(request, messages.ERROR, _('Exporting is not implemented for this provider')) else: - form = ExportForm() + form = ExportForm(user=request.user) recipe = request.GET.get('r') if recipe: if re.match(r'^([0-9])+$', recipe): if recipe := Recipe.objects.filter(pk=int(recipe)).first(): - form = ExportForm(initial={'recipes': recipe}) + form = ExportForm(initial={'recipes': recipe}, user=request.user) return render(request, 'export.html', {'form': form}) diff --git a/requirements.txt b/requirements.txt index 61dce5f96..367b83179 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,3 +31,4 @@ Jinja2==2.11.3 django-webpack-loader==0.7.0 django-js-reverse==0.9.1 django-allauth==0.44.0 +django-scopes==1.2.0 \ No newline at end of file