diff --git a/cookbook/admin.py b/cookbook/admin.py index 76af757a6..8893a7a09 100644 --- a/cookbook/admin.py +++ b/cookbook/admin.py @@ -1,4 +1,6 @@ from django.contrib import admin +from django.contrib.auth.admin import UserAdmin +from django.contrib.auth.models import User, Group from .models import (Comment, CookLog, Food, Ingredient, InviteLink, Keyword, MealPlan, MealType, NutritionInformation, Recipe, @@ -8,6 +10,17 @@ from .models import (Comment, CookLog, Food, Ingredient, InviteLink, Keyword, ViewLog, Supermarket, SupermarketCategory, SupermarketCategoryRelation) +class CustomUserAdmin(UserAdmin): + def has_add_permission(self, request, obj=None): + return False + + +admin.site.unregister(User) +admin.site.register(User, CustomUserAdmin) + +admin.site.unregister(Group) + + class SpaceAdmin(admin.ModelAdmin): list_display = ('name', 'message') @@ -16,10 +29,7 @@ admin.site.register(Space, SpaceAdmin) class UserPreferenceAdmin(admin.ModelAdmin): - list_display = ( - 'name', 'theme', 'nav_color', - 'default_page', 'search_style', 'comments' - ) + list_display = ('name', 'space', 'theme', 'nav_color', 'default_page', 'search_style',) @staticmethod def name(obj): diff --git a/cookbook/forms.py b/cookbook/forms.py index 1e68f42c6..73a80417a 100644 --- a/cookbook/forms.py +++ b/cookbook/forms.py @@ -6,7 +6,7 @@ from emoji_picker.widgets import EmojiPickerTextInput from .models import (Comment, Food, InviteLink, Keyword, MealPlan, Recipe, RecipeBook, RecipeBookEntry, Storage, Sync, Unit, User, - UserPreference, SupermarketCategory, MealType) + UserPreference, SupermarketCategory, MealType, Space) class SelectWidget(widgets.Select): @@ -371,12 +371,20 @@ class MealPlanForm(forms.ModelForm): class InviteLinkForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + user = kwargs.pop('user') + super().__init__(*args, **kwargs) + self.fields['space'].queryset = Space.objects.filter(created_by=user).all() + class Meta: model = InviteLink - fields = ('username', 'group', 'valid_until') + fields = ('username', 'group', 'valid_until', 'space') help_texts = { 'username': _('A username is not required, if left blank the new user can choose one.') # noqa: E501 } + field_classes = { + 'space': SafeModelChoiceField, + } class UserCreateForm(forms.Form): diff --git a/cookbook/helper/permission_helper.py b/cookbook/helper/permission_helper.py index fa9e12009..ebd7d0828 100644 --- a/cookbook/helper/permission_helper.py +++ b/cookbook/helper/permission_helper.py @@ -123,9 +123,13 @@ class GroupRequiredMixin(object): 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')) + try: + obj = self.get_object() + if obj.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')) + except AttributeError: + pass return super(GroupRequiredMixin, self).dispatch(request, *args, **kwargs) @@ -141,9 +145,13 @@ class OwnerRequiredMixin(object): messages.add_message(request, messages.ERROR, _('You cannot interact with this object as it is not owned by you!')) return HttpResponseRedirect(reverse('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')) + try: + obj = self.get_object() + if obj.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')) + except AttributeError: + pass return super(OwnerRequiredMixin, self).dispatch(request, *args, **kwargs) diff --git a/cookbook/helper/scope_middleware.py b/cookbook/helper/scope_middleware.py index 2ec472f49..9726d5263 100644 --- a/cookbook/helper/scope_middleware.py +++ b/cookbook/helper/scope_middleware.py @@ -1,3 +1,5 @@ +from django.shortcuts import redirect +from django.urls import reverse from django_scopes import scope, scopes_disabled @@ -7,10 +9,21 @@ class ScopeMiddleware: def __call__(self, request): if request.user.is_authenticated: - request.space = request.user.userpreference.space + + if request.user.groups.count() == 0: + return redirect('view_no_group') with scopes_disabled(): - #with scope(space=request.space): + if request.user.userpreference.space is None and not reverse('view_no_space') in request.path and not reverse('account_logout') in request.path: + return redirect(reverse('view_no_space')) + + if request.path.startswith('/admin/'): + with scopes_disabled(): + return self.get_response(request) + + request.space = request.user.userpreference.space + # with scopes_disabled(): + with scope(space=request.space): return self.get_response(request) else: return self.get_response(request) diff --git a/cookbook/integration/integration.py b/cookbook/integration/integration.py index 49329e41f..49200d546 100644 --- a/cookbook/integration/integration.py +++ b/cookbook/integration/integration.py @@ -27,7 +27,8 @@ class Integration: self.keyword = Keyword.objects.create( name=f'Import {date_format(datetime.datetime.now(), "DATETIME_FORMAT")}.{datetime.datetime.now().strftime("%S")}', description=f'Imported by {request.user.get_user_name()} at {date_format(datetime.datetime.now(), "DATETIME_FORMAT")}', - icon='📥' + icon='📥', + space=request.space ) def do_export(self, recipes): diff --git a/cookbook/migrations/0109_auto_20210221_1204.py b/cookbook/migrations/0109_auto_20210221_1204.py new file mode 100644 index 000000000..37308fa76 --- /dev/null +++ b/cookbook/migrations/0109_auto_20210221_1204.py @@ -0,0 +1,63 @@ +# Generated by Django 3.1.6 on 2021-02-21 11:04 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cookbook', '0108_auto_20210219_1410'), + ] + + operations = [ + migrations.RemoveField( + model_name='recipebookentry', + name='space', + ), + migrations.AlterField( + model_name='food', + name='name', + field=models.CharField(max_length=128, validators=[django.core.validators.MinLengthValidator(1)]), + ), + migrations.AlterField( + model_name='keyword', + name='name', + field=models.CharField(max_length=64), + ), + migrations.AlterField( + model_name='supermarket', + name='name', + field=models.CharField(max_length=128, validators=[django.core.validators.MinLengthValidator(1)]), + ), + migrations.AlterField( + model_name='supermarketcategory', + name='name', + field=models.CharField(max_length=128, validators=[django.core.validators.MinLengthValidator(1)]), + ), + migrations.AlterField( + model_name='unit', + name='name', + field=models.CharField(max_length=128, validators=[django.core.validators.MinLengthValidator(1)]), + ), + migrations.AlterUniqueTogether( + name='food', + unique_together={('space', 'name')}, + ), + migrations.AlterUniqueTogether( + name='keyword', + unique_together={('space', 'name')}, + ), + migrations.AlterUniqueTogether( + name='supermarket', + unique_together={('space', 'name')}, + ), + migrations.AlterUniqueTogether( + name='supermarketcategory', + unique_together={('space', 'name')}, + ), + migrations.AlterUniqueTogether( + name='unit', + unique_together={('space', 'name')}, + ), + ] diff --git a/cookbook/migrations/0110_auto_20210221_1406.py b/cookbook/migrations/0110_auto_20210221_1406.py new file mode 100644 index 000000000..47e002ff8 --- /dev/null +++ b/cookbook/migrations/0110_auto_20210221_1406.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.6 on 2021-02-21 13:06 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('cookbook', '0109_auto_20210221_1204'), + ] + + operations = [ + migrations.AlterField( + model_name='userpreference', + name='space', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'), + ), + ] diff --git a/cookbook/migrations/0111_space_created_by.py b/cookbook/migrations/0111_space_created_by.py new file mode 100644 index 000000000..7f42a74e5 --- /dev/null +++ b/cookbook/migrations/0111_space_created_by.py @@ -0,0 +1,32 @@ +# Generated by Django 3.1.6 on 2021-02-21 13:19 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +from django_scopes import scopes_disabled + + +def set_default_owner(apps, schema_editor): + Space = apps.get_model('cookbook', 'Space') + User = apps.get_model('auth', 'user') + + with scopes_disabled(): + for x in Space.objects.all(): + x.created_by = User.objects.filter(is_superuser=True).first() + x.save() + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('cookbook', '0110_auto_20210221_1406'), + ] + + operations = [ + migrations.AddField( + model_name='space', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), + ), + migrations.RunPython(set_default_owner), + ] diff --git a/cookbook/models.py b/cookbook/models.py index dd3b10985..e89e7745a 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -45,8 +45,12 @@ class PermissionModelMixin: class Space(models.Model): name = models.CharField(max_length=128, default='Default') + created_by = models.ForeignKey(User, on_delete=models.PROTECT, null=True) message = models.CharField(max_length=512, default='', blank=True) + def __str__(self): + return self.name + class UserPreference(models.Model, PermissionModelMixin): # Themes @@ -121,7 +125,7 @@ class UserPreference(models.Model, PermissionModelMixin): shopping_auto_sync = models.IntegerField(default=5) sticky_navbar = models.BooleanField(default=STICKY_NAV_PREF_DEFAULT) - space = models.ForeignKey(Space, on_delete=models.CASCADE) + space = models.ForeignKey(Space, on_delete=models.CASCADE, null=True) objects = ScopedManager(space='space') def __str__(self): @@ -168,7 +172,7 @@ class Sync(models.Model, PermissionModelMixin): class SupermarketCategory(models.Model, PermissionModelMixin): - name = models.CharField(unique=True, max_length=128, validators=[MinLengthValidator(1)]) + name = models.CharField(max_length=128, validators=[MinLengthValidator(1)]) description = models.TextField(blank=True, null=True) space = models.ForeignKey(Space, on_delete=models.CASCADE) @@ -177,9 +181,12 @@ class SupermarketCategory(models.Model, PermissionModelMixin): def __str__(self): return self.name + class Meta: + unique_together = (('space', 'name'),) + class Supermarket(models.Model, PermissionModelMixin): - name = models.CharField(unique=True, max_length=128, validators=[MinLengthValidator(1)]) + name = models.CharField(max_length=128, validators=[MinLengthValidator(1)]) description = models.TextField(blank=True, null=True) categories = models.ManyToManyField(SupermarketCategory, through='SupermarketCategoryRelation') @@ -189,6 +196,9 @@ class Supermarket(models.Model, PermissionModelMixin): def __str__(self): return self.name + class Meta: + unique_together = (('space', 'name'),) + class SupermarketCategoryRelation(models.Model, PermissionModelMixin): supermarket = models.ForeignKey(Supermarket, on_delete=models.CASCADE, related_name='category_to_supermarket') @@ -218,7 +228,7 @@ class SyncLog(models.Model, PermissionModelMixin): class Keyword(models.Model, PermissionModelMixin): - name = models.CharField(max_length=64, unique=True) + name = models.CharField(max_length=64) icon = models.CharField(max_length=16, blank=True, null=True) description = models.TextField(default="", blank=True) created_at = models.DateTimeField(auto_now_add=True) @@ -233,9 +243,12 @@ class Keyword(models.Model, PermissionModelMixin): else: return f"{self.name}" + class Meta: + unique_together = (('space', 'name'),) + class Unit(models.Model, PermissionModelMixin): - name = models.CharField(unique=True, max_length=128, validators=[MinLengthValidator(1)]) + name = models.CharField(max_length=128, validators=[MinLengthValidator(1)]) description = models.TextField(blank=True, null=True) space = models.ForeignKey(Space, on_delete=models.CASCADE) @@ -244,9 +257,12 @@ class Unit(models.Model, PermissionModelMixin): def __str__(self): return self.name + class Meta: + unique_together = (('space', 'name'),) + class Food(models.Model, PermissionModelMixin): - name = models.CharField(unique=True, max_length=128, validators=[MinLengthValidator(1)]) + name = models.CharField(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) ignore_shopping = models.BooleanField(default=False) @@ -258,6 +274,9 @@ class Food(models.Model, PermissionModelMixin): def __str__(self): return self.name + class Meta: + unique_together = (('space', 'name'),) + class Ingredient(models.Model, PermissionModelMixin): food = models.ForeignKey(Food, on_delete=models.PROTECT, null=True, blank=True) diff --git a/cookbook/serializer.py b/cookbook/serializer.py index f52c2c926..436f849df 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -111,17 +111,12 @@ class KeywordSerializer(UniqueFieldsMixin, serializers.ModelSerializer): return str(obj) def create(self, validated_data): - # since multi select tags dont have id's - # duplicate names might be routed to create - obj, created = Keyword.objects.get_or_create(name=validated_data['name']) + obj, created = Keyword.objects.get_or_create(name=validated_data['name'], space=self.context['request'].space) return obj class Meta: model = Keyword - fields = ( - 'id', 'name', 'icon', 'label', 'description', - 'created_at', 'updated_at' - ) + fields = ('id', 'name', 'icon', 'label', 'description', 'created_at', 'updated_at') read_only_fields = ('id',) @@ -129,9 +124,7 @@ class KeywordSerializer(UniqueFieldsMixin, serializers.ModelSerializer): class UnitSerializer(UniqueFieldsMixin, serializers.ModelSerializer): def create(self, validated_data): - # since multi select tags dont have id's - # duplicate names might be routed to create - obj, created = Unit.objects.get_or_create(name=validated_data['name']) + obj, created = Unit.objects.get_or_create(name=validated_data['name'], space=self.context['request'].space) return obj class Meta: @@ -143,9 +136,7 @@ class UnitSerializer(UniqueFieldsMixin, serializers.ModelSerializer): class SupermarketCategorySerializer(UniqueFieldsMixin, WritableNestedModelSerializer): def create(self, validated_data): - # since multi select tags dont have id's - # duplicate names might be routed to create - obj, created = SupermarketCategory.objects.get_or_create(name=validated_data['name']) + obj, created = SupermarketCategory.objects.get_or_create(name=validated_data['name'], space=self.context['request'].space) return obj def update(self, instance, validated_data): @@ -176,9 +167,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer): supermarket_category = SupermarketCategorySerializer(allow_null=True, required=False) def create(self, validated_data): - # since multi select tags dont have id's - # duplicate names might be routed to create - obj, created = Food.objects.get_or_create(name=validated_data['name']) + obj, created = Food.objects.get_or_create(name=validated_data['name'], space=self.context['request'].space) return obj def update(self, instance, validated_data): @@ -256,6 +245,7 @@ class RecipeSerializer(WritableNestedModelSerializer): def create(self, validated_data): validated_data['created_by'] = self.context['request'].user + validated_data['space'] = self.context['request'].space return super().create(validated_data) @@ -455,4 +445,5 @@ class RecipeExportSerializer(WritableNestedModelSerializer): def create(self, validated_data): validated_data['created_by'] = self.context['request'].user + validated_data['space'] = self.context['request'].space return super().create(validated_data) diff --git a/cookbook/templates/no_groups_info.html b/cookbook/templates/no_groups_info.html index 53b5f04c2..eac51be12 100644 --- a/cookbook/templates/no_groups_info.html +++ b/cookbook/templates/no_groups_info.html @@ -2,7 +2,7 @@ {% load static %} {% load i18n %} -{% block title %}{% trans "Offline" %}{% endblock %} +{% block title %}{% trans "No Permissions" %}{% endblock %} {% block content %} diff --git a/cookbook/templates/no_space_info.html b/cookbook/templates/no_space_info.html new file mode 100644 index 000000000..f8ad26e84 --- /dev/null +++ b/cookbook/templates/no_space_info.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} +{% load static %} +{% load i18n %} + +{% block title %}{% trans "No Space" %}{% endblock %} + + +{% block content %} + +