lots of fixes and stuff

This commit is contained in:
vabene1111 2022-06-06 18:21:15 +02:00
parent 07f78bb7b8
commit e2b887b449
19 changed files with 236 additions and 189 deletions

View File

@ -32,41 +32,7 @@ admin.site.unregister(Group)
@admin.action(description='Delete all data from a space') @admin.action(description='Delete all data from a space')
def delete_space_action(modeladmin, request, queryset): def delete_space_action(modeladmin, request, queryset):
for space in queryset: for space in queryset:
CookLog.objects.filter(space=space).delete() space.save()
ViewLog.objects.filter(space=space).delete()
ImportLog.objects.filter(space=space).delete()
BookmarkletImport.objects.filter(space=space).delete()
Comment.objects.filter(recipe__space=space).delete()
Keyword.objects.filter(space=space).delete()
Ingredient.objects.filter(space=space).delete()
Food.objects.filter(space=space).delete()
Unit.objects.filter(space=space).delete()
Step.objects.filter(space=space).delete()
NutritionInformation.objects.filter(space=space).delete()
RecipeBookEntry.objects.filter(book__space=space).delete()
RecipeBook.objects.filter(space=space).delete()
MealType.objects.filter(space=space).delete()
MealPlan.objects.filter(space=space).delete()
ShareLink.objects.filter(space=space).delete()
Recipe.objects.filter(space=space).delete()
RecipeImport.objects.filter(space=space).delete()
SyncLog.objects.filter(sync__space=space).delete()
Sync.objects.filter(space=space).delete()
Storage.objects.filter(space=space).delete()
ShoppingListEntry.objects.filter(shoppinglist__space=space).delete()
ShoppingListRecipe.objects.filter(shoppinglist__space=space).delete()
ShoppingList.objects.filter(space=space).delete()
SupermarketCategoryRelation.objects.filter(supermarket__space=space).delete()
SupermarketCategory.objects.filter(space=space).delete()
Supermarket.objects.filter(space=space).delete()
InviteLink.objects.filter(space=space).delete()
UserFile.objects.filter(space=space).delete()
Automation.objects.filter(space=space).delete()
class SpaceAdmin(admin.ModelAdmin): class SpaceAdmin(admin.ModelAdmin):

View File

@ -760,6 +760,6 @@ def old_search(request):
params = dict(request.GET) params = dict(request.GET)
params['internal'] = None params['internal'] = None
f = RecipeFilter(params, f = RecipeFilter(params,
queryset=Recipe.objects.filter(space=request.user.userpreference.space).all().order_by(Lower('name').asc()), queryset=Recipe.objects.filter(space=request.space).all().order_by(Lower('name').asc()),
space=request.space) space=request.space)
return f.qs return f.qs

View File

@ -4,11 +4,12 @@ from django.db import migrations
def create_default_space(apps, schema_editor): def create_default_space(apps, schema_editor):
Space = apps.get_model('cookbook', 'Space') # Space = apps.get_model('cookbook', 'Space')
Space.objects.create( # Space.objects.create(
name='Default', # name='Default',
message='' # message=''
) # )
pass # Beginning with the multi space tenancy version (~something around 1.3) a default space is no longer needed as the first user can create it after setup
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -240,7 +240,49 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
demo = models.BooleanField(default=False) demo = models.BooleanField(default=False)
food_inherit = models.ManyToManyField(FoodInheritField, blank=True) food_inherit = models.ManyToManyField(FoodInheritField, blank=True)
show_facet_count = models.BooleanField(default=False) show_facet_count = models.BooleanField(default=False)
def safe_delete(self):
"""
Safely deletes a space by deleting all objects belonging to the space first and then deleting the space itself
"""
CookLog.objects.filter(space=self).delete()
ViewLog.objects.filter(space=self).delete()
ImportLog.objects.filter(space=self).delete()
BookmarkletImport.objects.filter(space=self).delete()
CustomFilter.objects.filter(space=self).delete()
Comment.objects.filter(recipe__space=self).delete()
Keyword.objects.filter(space=self).delete()
Ingredient.objects.filter(space=self).delete()
Food.objects.filter(space=self).delete()
Unit.objects.filter(space=self).delete()
Step.objects.filter(space=self).delete()
NutritionInformation.objects.filter(space=self).delete()
RecipeBookEntry.objects.filter(book__space=self).delete()
RecipeBook.objects.filter(space=self).delete()
MealType.objects.filter(space=self).delete()
MealPlan.objects.filter(space=self).delete()
ShareLink.objects.filter(space=self).delete()
Recipe.objects.filter(space=self).delete()
RecipeImport.objects.filter(space=self).delete()
SyncLog.objects.filter(sync__space=self).delete()
Sync.objects.filter(space=self).delete()
Storage.objects.filter(space=self).delete()
ShoppingListEntry.objects.filter(shoppinglist__space=self).delete()
ShoppingListRecipe.objects.filter(shoppinglist__space=self).delete()
ShoppingList.objects.filter(space=self).delete()
SupermarketCategoryRelation.objects.filter(supermarket__space=self).delete()
SupermarketCategory.objects.filter(space=self).delete()
Supermarket.objects.filter(space=self).delete()
InviteLink.objects.filter(space=self).delete()
UserFile.objects.filter(space=self).delete()
Automation.objects.filter(space=self).delete()
self.delete()
def get_owner(self): def get_owner(self):
return self.created_by return self.created_by
@ -551,14 +593,14 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
tree_filter = Q(space=space) tree_filter = Q(space=space)
# remove all inherited fields from food # remove all inherited fields from food
Through = Food.objects.filter(tree_filter).first().inherit_fields.through trough = Food.objects.filter(tree_filter).first().inherit_fields.through
Through.objects.all().delete() trough.objects.all().delete()
# food is going to inherit attributes # food is going to inherit attributes
if len(inherit) > 0: if len(inherit) > 0:
# ManyToMany cannot be updated through an UPDATE operation # ManyToMany cannot be updated through an UPDATE operation
for i in inherit: for i in inherit:
Through.objects.bulk_create([ trough.objects.bulk_create([
Through(food_id=x, foodinheritfield_id=i['id']) trough(food_id=x, foodinheritfield_id=i['id'])
for x in Food.objects.filter(tree_filter).values_list('id', flat=True) for x in Food.objects.filter(tree_filter).values_list('id', flat=True)
]) ])

View File

@ -151,10 +151,27 @@ class GroupSerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
fields = ('id', 'name') fields = ('id', 'name')
class SpaceSerializer(serializers.ModelSerializer): class FoodInheritFieldSerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
name = serializers.CharField(allow_null=True, allow_blank=True, required=False)
field = serializers.CharField(allow_null=True, allow_blank=True, required=False)
def create(self, validated_data):
raise ValidationError('Cannot create using this endpoint')
def update(self, instance, validated_data):
return instance
class Meta:
model = FoodInheritField
fields = ('id', 'name', 'field',)
read_only_fields = ['id']
class SpaceSerializer(WritableNestedModelSerializer):
user_count = serializers.SerializerMethodField('get_user_count') user_count = serializers.SerializerMethodField('get_user_count')
recipe_count = serializers.SerializerMethodField('get_recipe_count') recipe_count = serializers.SerializerMethodField('get_recipe_count')
file_size_mb = serializers.SerializerMethodField('get_file_size_mb') file_size_mb = serializers.SerializerMethodField('get_file_size_mb')
food_inherit = FoodInheritFieldSerializer(many=True)
def get_user_count(self, obj): def get_user_count(self, obj):
return UserSpace.objects.filter(space=obj).count() return UserSpace.objects.filter(space=obj).count()
@ -174,13 +191,18 @@ class SpaceSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Space model = Space
fields = ('id', 'name', 'created_by', 'created_at', 'message', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing', 'demo', 'food_inherit', 'show_facet_count', 'user_count', 'recipe_count', 'file_size_mb',) fields = ('id', 'name', 'created_by', 'created_at', 'message', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing', 'demo', 'food_inherit', 'show_facet_count', 'user_count', 'recipe_count', 'file_size_mb',)
read_only_fields = ('id', 'created_by', 'created_at', 'message', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing', 'demo',) read_only_fields = ('id', 'created_by', 'created_at', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing', 'demo',)
class UserSpaceSerializer(WritableNestedModelSerializer): class UserSpaceSerializer(WritableNestedModelSerializer):
user = UserNameSerializer(read_only=True) user = UserNameSerializer(read_only=True)
groups = GroupSerializer(many=True) groups = GroupSerializer(many=True)
def validate(self, data):
if self.instance.user == self.context['request'].space.created_by: # cant change space owner permission
raise serializers.ValidationError(_('Cannot modify Space owner permission.'))
return super().validate(data)
def create(self, validated_data): def create(self, validated_data):
raise ValidationError('Cannot create using this endpoint') raise ValidationError('Cannot create using this endpoint')
@ -209,24 +231,6 @@ class MealTypeSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
read_only_fields = ('created_by',) read_only_fields = ('created_by',)
class FoodInheritFieldSerializer(WritableNestedModelSerializer):
name = serializers.CharField(allow_null=True, allow_blank=True, required=False)
field = serializers.CharField(allow_null=True, allow_blank=True, required=False)
def create(self, validated_data):
# don't allow writing to FoodInheritField via API
return FoodInheritField.objects.get(**validated_data)
def update(self, instance, validated_data):
# don't allow writing to FoodInheritField via API
return FoodInheritField.objects.get(**validated_data)
class Meta:
model = FoodInheritField
fields = ('id', 'name', 'field',)
read_only_fields = ['id']
class UserPreferenceSerializer(WritableNestedModelSerializer): class UserPreferenceSerializer(WritableNestedModelSerializer):
food_inherit_default = FoodInheritFieldSerializer(source='space.food_inherit', many=True, allow_null=True, food_inherit_default = FoodInheritFieldSerializer(source='space.food_inherit', many=True, allow_null=True,
required=False, read_only=True) required=False, read_only=True)
@ -1073,7 +1077,7 @@ class InviteLinkSerializer(WritableNestedModelSerializer):
class BookmarkletImportListSerializer(serializers.ModelSerializer): class BookmarkletImportListSerializer(serializers.ModelSerializer):
def create(self, validated_data): def create(self, validated_data):
validated_data['created_by'] = self.context['request'].user validated_data['created_by'] = self.context['request'].user
validated_data['space'] = self.context['request'].user.userpreference.space validated_data['space'] = self.context['request'].space
return super().create(validated_data) return super().create(validated_data)
class Meta: class Meta:

View File

@ -2,6 +2,7 @@ from decimal import Decimal
from functools import wraps from functools import wraps
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.postgres.search import SearchVector from django.contrib.postgres.search import SearchVector
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
@ -11,7 +12,7 @@ from django_scopes import scope
from cookbook.helper.shopping_helper import RecipeShoppingEditor from cookbook.helper.shopping_helper import RecipeShoppingEditor
from cookbook.managers import DICTIONARY from cookbook.managers import DICTIONARY
from cookbook.models import (Food, FoodInheritField, Ingredient, MealPlan, Recipe, from cookbook.models import (Food, FoodInheritField, Ingredient, MealPlan, Recipe,
ShoppingListEntry, Step) ShoppingListEntry, Step, UserPreference)
SQLITE = True SQLITE = True
if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2', if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2',
@ -28,9 +29,17 @@ def skip_signal(signal_func):
if hasattr(instance, 'skip_signal'): if hasattr(instance, 'skip_signal'):
return None return None
return signal_func(sender, instance, **kwargs) return signal_func(sender, instance, **kwargs)
return _decorator return _decorator
@receiver(post_save, sender=User)
@skip_signal
def update_recipe_search_vector(sender, instance=None, created=False, **kwargs):
if created:
UserPreference.objects.get_or_create(user=instance)
@receiver(post_save, sender=Recipe) @receiver(post_save, sender=Recipe)
@skip_signal @skip_signal
def update_recipe_search_vector(sender, instance=None, created=False, **kwargs): def update_recipe_search_vector(sender, instance=None, created=False, **kwargs):
@ -131,5 +140,3 @@ def auto_add_shopping(sender, instance=None, created=False, weak=False, **kwargs
print("MEAL_AUTO_ADD Created SLR") print("MEAL_AUTO_ADD Created SLR")
except AttributeError: except AttributeError:
pass pass

View File

@ -294,7 +294,7 @@
<a class="dropdown-item" href="{% url 'data_sync' %}"><i <a class="dropdown-item" href="{% url 'data_sync' %}"><i
class="fas fa-sync-alt fa-fw"></i> {% trans 'External Recipes' %}</a> class="fas fa-sync-alt fa-fw"></i> {% trans 'External Recipes' %}</a>
{% if request.user == request.space.created_by %} {% if request.user == request.space.created_by %}
<a class="dropdown-item" href="{% url 'view_space' %}"><i <a class="dropdown-item" href="{% url 'view_space_manage' request.space.pk %}"><i
class="fas fa-server fa-fw"></i> {% trans 'Space Settings' %}</a> class="fas fa-server fa-fw"></i> {% trans 'Space Settings' %}</a>
{% endif %} {% endif %}
{% if user.is_superuser %} {% if user.is_superuser %}
@ -316,7 +316,7 @@
{% endif %} {% endif %}
{{ us.space.name }}</a> {{ us.space.name }}</a>
{% endfor %} {% endfor %}
<a class="dropdown-item" href="{% url 'view_space_overview' %}"><i class="fas fa-plus fa-fw"></i> {% trans 'Create New' %}</a> <a class="dropdown-item" href="{% url 'view_space_overview' %}"><i class="fas fa-list"></i> {% trans 'Overview' %}</a>
{% endif %} {% endif %}
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item" href="{% url 'docs_markdown' %}"><i <a class="dropdown-item" href="{% url 'docs_markdown' %}"><i
@ -344,7 +344,7 @@
</div> </div>
</nav> </nav>
{% message_of_the_day as message_of_the_day %} {% message_of_the_day request as message_of_the_day %}
{% if message_of_the_day %} {% if message_of_the_day %}
<div class="bg-success" style=" width: 100%; text-align: center!important; color: #ffffff; padding: 8px"> <div class="bg-success" style=" width: 100%; text-align: center!important; color: #ffffff; padding: 8px">
{{ message_of_the_day }} {{ message_of_the_day }}

View File

@ -17,7 +17,7 @@
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'view_space' %}">{% trans 'Space Settings' %}</a></li> <li class="breadcrumb-item"><a href="{% url 'view_space_manage' request.space.pk %}">{% trans 'Space Settings' %}</a></li>
</ol> </ol>
</nav> </nav>

View File

@ -12,7 +12,7 @@
<div class="col col-12"> <div class="col col-12">
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'view_space' %}">{% trans 'Space Settings' %}</a></li> <li class="breadcrumb-item"><a href="{% url 'view_space_manage' request.space.pk %}">{% trans 'Space Settings' %}</a></li>
</ol> </ol>
</nav> </nav>
</div> </div>

View File

@ -3,7 +3,7 @@
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "No Space" %}{% endblock %} {% block title %}{% trans "Overview" %}{% endblock %}
{% block content %} {% block content %}
@ -36,6 +36,11 @@
<h5 class="card-title"><a <h5 class="card-title"><a
href="{% url 'view_switch_space' us.space.id %}">{{ us.space.name }}</a> href="{% url 'view_switch_space' us.space.id %}">{{ us.space.name }}</a>
</h5> </h5>
{# {% if us.active %}#}
{# <i class="far fa-dot-circle fa-fw"></i>#}
{# {% else %}#}
{# <i class="far fa-circle fa-fw"></i>#}
{# {% endif %}#}
<p class="card-text"><small <p class="card-text"><small
class="text-muted">{% trans 'Owner' %}: {{ us.space.created_by }}</small> class="text-muted">{% trans 'Owner' %}: {{ us.space.created_by }}</small>
{% if us.space.created_by != us.user %} {% if us.space.created_by != us.user %}

View File

@ -111,8 +111,12 @@ def page_help(page_name):
@register.simple_tag @register.simple_tag
def message_of_the_day(): def message_of_the_day(request):
return Space.objects.first().message try:
if request.space.message:
return request.space.message
except (AttributeError, KeyError, ValueError):
pass
@register.simple_tag @register.simple_tag

View File

@ -56,9 +56,9 @@ def test_get_related_recipes(request, arg, recipe, related_count, u1_s1, space_2
({'steps__food_recipe_count': {'step': 0, 'count': 1}}), # shopping list from recipe with food recipe ({'steps__food_recipe_count': {'step': 0, 'count': 1}}), # shopping list from recipe with food recipe
({'steps__food_recipe_count': {'step': 0, 'count': 1}, 'steps__recipe_count': 1}), # shopping list from recipe with StepRecipe and food recipe ({'steps__food_recipe_count': {'step': 0, 'count': 1}, 'steps__recipe_count': 1}), # shopping list from recipe with StepRecipe and food recipe
], indirect=['recipe']) ], indirect=['recipe'])
def test_related_mixed_space(request, recipe, u1_s2): def test_related_mixed_space(request, recipe, u1_s2, space_2):
with scopes_disabled(): with scopes_disabled():
recipe.space = auth.get_user(u1_s2).userpreference.space recipe.space = space_2
recipe.save() recipe.save()
assert len(json.loads( assert len(json.loads(
u1_s2.get( u1_s2.get(

View File

@ -204,11 +204,11 @@ def test_shopping_recipe_userpreference(recipe, sle_count, use_mealplan, user2):
assert len(json.loads(user2.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count[1] assert len(json.loads(user2.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count[1]
def test_shopping_recipe_mixed_authors(u1_s1, u2_s1): def test_shopping_recipe_mixed_authors(u1_s1, u2_s1,space_1):
with scopes_disabled(): with scopes_disabled():
user1 = auth.get_user(u1_s1) user1 = auth.get_user(u1_s1)
user2 = auth.get_user(u2_s1) user2 = auth.get_user(u2_s1)
space = user1.userpreference.space space = space_1
user3 = UserFactory(space=space) user3 = UserFactory(space=space)
recipe1 = RecipeFactory(created_by=user1, space=space) recipe1 = RecipeFactory(created_by=user1, space=space)
recipe2 = RecipeFactory(created_by=user2, space=space) recipe2 = RecipeFactory(created_by=user2, space=space)

View File

@ -12,7 +12,7 @@ from recipes.version import VERSION_NUMBER
from .models import (Automation, Comment, CustomFilter, Food, InviteLink, Keyword, MealPlan, Recipe, from .models import (Automation, Comment, CustomFilter, Food, InviteLink, Keyword, MealPlan, Recipe,
RecipeBook, RecipeBookEntry, RecipeImport, ShoppingList, Step, Storage, RecipeBook, RecipeBookEntry, RecipeImport, ShoppingList, Step, Storage,
Supermarket, SupermarketCategory, Sync, SyncLog, Unit, UserFile, Supermarket, SupermarketCategory, Sync, SyncLog, Unit, UserFile,
get_model_name, UserSpace) get_model_name, UserSpace, Space)
from .views import api, data, delete, edit, import_export, lists, new, telegram, views from .views import api, data, delete, edit, import_export, lists, new, telegram, views
from .views.api import CustomAuthToken from .views.api import CustomAuthToken
@ -55,15 +55,11 @@ router.register(r'view-log', api.ViewLogViewSet)
urlpatterns = [ urlpatterns = [
path('', views.index, name='index'), path('', views.index, name='index'),
path('setup/', views.setup, name='view_setup'), path('setup/', views.setup, name='view_setup'),
path('space/', views.space, name='view_space'),
path('space/member/<int:user_id>/<int:space_id>/<slug:group>', views.space_change_member,
name='change_space_member'),
path('no-group', views.no_groups, name='view_no_group'), path('no-group', views.no_groups, name='view_no_group'),
path('space-overview', views.space_overview, name='view_space_overview'), path('space-overview', views.space_overview, name='view_space_overview'),
path('space-manage/<int:space_id>', views.space_manage, name='view_space_manage'), path('space-manage/<int:space_id>', views.space_manage, name='view_space_manage'),
path('switch-space/<int:space_id>', views.switch_space, name='view_switch_space'), path('switch-space/<int:space_id>', views.switch_space, name='view_switch_space'),
path('no-perm', views.no_perm, name='view_no_perm'), path('no-perm', views.no_perm, name='view_no_perm'),
path('signup/<slug:token>', views.signup, name='view_signup'), # TODO deprecated with 0.16.2 remove at some point
path('invite/<slug:token>', views.invite_link, name='view_invite'), path('invite/<slug:token>', views.invite_link, name='view_invite'),
path('system/', views.system, name='view_system'), path('system/', views.system, name='view_system'),
path('search/', views.search, name='view_search'), path('search/', views.search, name='view_search'),
@ -120,6 +116,7 @@ urlpatterns = [
path('api/ingredient-from-string/', api.ingredient_from_string, name='api_ingredient_from_string'), path('api/ingredient-from-string/', api.ingredient_from_string, name='api_ingredient_from_string'),
path('api/share-link/<int:pk>', api.share_link, name='api_share_link'), path('api/share-link/<int:pk>', api.share_link, name='api_share_link'),
path('api/get_facets/', api.get_facets, name='api_get_facets'), path('api/get_facets/', api.get_facets, name='api_get_facets'),
path('api/reset-food-inheritance/', api.reset_food_inheritance, name='api_reset_food_inheritance'),
path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'), path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'),
# TODO is this deprecated? not yet, some old forms remain, could likely be changed to generic API endpoints # TODO is this deprecated? not yet, some old forms remain, could likely be changed to generic API endpoints
@ -151,7 +148,7 @@ urlpatterns = [
generic_models = ( generic_models = (
Recipe, RecipeImport, Storage, RecipeBook, MealPlan, SyncLog, Sync, Recipe, RecipeImport, Storage, RecipeBook, MealPlan, SyncLog, Sync,
Comment, RecipeBookEntry, ShoppingList, InviteLink, UserSpace Comment, RecipeBookEntry, ShoppingList, InviteLink, UserSpace, Space
) )
for m in generic_models: for m in generic_models:

View File

@ -2,6 +2,7 @@ import io
import json import json
import mimetypes import mimetypes
import re import re
import traceback
import uuid import uuid
from collections import OrderedDict from collections import OrderedDict
@ -172,9 +173,9 @@ class FuzzyFilterMixin(ViewSetMixin, ExtendedRecipeMixin):
self.queryset = ( self.queryset = (
self.queryset self.queryset
.annotate(starts=Case(When(name__istartswith=query, then=(Value(100))), .annotate(starts=Case(When(name__istartswith=query, then=(Value(100))),
default=Value(0))) # put exact matches at the top of the result set default=Value(0))) # put exact matches at the top of the result set
.filter(filter).order_by('-starts', Lower('name').asc()) .filter(filter).order_by('-starts', Lower('name').asc())
) )
updated_at = self.request.query_params.get('updated_at', None) updated_at = self.request.query_params.get('updated_at', None)
@ -388,6 +389,11 @@ class UserSpaceViewSet(viewsets.ModelViewSet):
permission_classes = [CustomIsSpaceOwner] permission_classes = [CustomIsSpaceOwner]
http_method_names = ['get', 'patch', 'put', 'delete'] http_method_names = ['get', 'patch', 'put', 'delete']
def destroy(self, request, *args, **kwargs):
if request.space.created_by == UserSpace.objects.get(pk=kwargs['pk']).user:
raise APIException('Cannot delete Space owner permission.')
return super().destroy(request, *args, **kwargs)
def get_queryset(self): def get_queryset(self):
return self.queryset.filter(space=self.request.space) return self.queryset.filter(space=self.request.space)
@ -1156,6 +1162,22 @@ def recipe_from_source(request):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@api_view(['GET'])
# @schema(AutoSchema()) #TODO add proper schema
@permission_classes([CustomIsAdmin])
# TODO add rate limiting
def reset_food_inheritance(request):
"""
function to reset inheritance from api, see food method for docs
"""
try:
Food.reset_inheritance(space=request.space)
return Response({'message': 'success', }, status=status.HTTP_200_OK)
except Exception as e:
traceback.print_exc()
return Response(str(e), status=status.HTTP_400_BAD_REQUEST)
def get_recipe_provider(recipe): def get_recipe_provider(recipe):
if recipe.storage.method == Storage.DROPBOX: if recipe.storage.method == Storage.DROPBOX:
return Dropbox return Dropbox

View File

@ -9,7 +9,7 @@ from django.views.generic import DeleteView
from cookbook.helper.permission_helper import GroupRequiredMixin, OwnerRequiredMixin, group_required from cookbook.helper.permission_helper import GroupRequiredMixin, OwnerRequiredMixin, group_required
from cookbook.models import (Comment, InviteLink, MealPlan, Recipe, RecipeBook, RecipeBookEntry, from cookbook.models import (Comment, InviteLink, MealPlan, Recipe, RecipeBook, RecipeBookEntry,
RecipeImport, Storage, Sync, UserSpace) RecipeImport, Storage, Sync, UserSpace, Space)
from cookbook.provider.dropbox import Dropbox from cookbook.provider.dropbox import Dropbox
from cookbook.provider.local import Local from cookbook.provider.local import Local
from cookbook.provider.nextcloud import Nextcloud from cookbook.provider.nextcloud import Nextcloud
@ -199,3 +199,19 @@ class UserSpaceDelete(OwnerRequiredMixin, DeleteView):
context = super(UserSpaceDelete, self).get_context_data(**kwargs) context = super(UserSpaceDelete, self).get_context_data(**kwargs)
context['title'] = _("Space Membership") context['title'] = _("Space Membership")
return context return context
class SpaceDelete(OwnerRequiredMixin, DeleteView):
template_name = "generic/delete_template.html"
model = Space
success_url = reverse_lazy('view_space_overview')
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
self.object.safe_delete()
return HttpResponseRedirect(self.get_success_url())
def get_context_data(self, **kwargs):
context = super(SpaceDelete, self).get_context_data(**kwargs)
context['title'] = _("Space")
return context

View File

@ -61,7 +61,7 @@ def search(request):
if request.user.userpreference.search_style == UserPreference.NEW: if request.user.userpreference.search_style == UserPreference.NEW:
return search_v2(request) return search_v2(request)
f = RecipeFilter(request.GET, f = RecipeFilter(request.GET,
queryset=Recipe.objects.filter(space=request.user.userpreference.space).all().order_by( queryset=Recipe.objects.filter(space=request.space).all().order_by(
Lower('name').asc()), Lower('name').asc()),
space=request.space) space=request.space)
if request.user.userpreference.search_style == UserPreference.LARGE: if request.user.userpreference.search_style == UserPreference.LARGE:
@ -72,7 +72,7 @@ def search(request):
if request.GET == {} and request.user.userpreference.show_recent: if request.GET == {} and request.user.userpreference.show_recent:
qs = Recipe.objects.filter(viewlog__created_by=request.user).filter( qs = Recipe.objects.filter(viewlog__created_by=request.user).filter(
space=request.user.userpreference.space).order_by('-viewlog__created_at').all() space=request.space).order_by('-viewlog__created_at').all()
recent_list = [] recent_list = []
for r in qs: for r in qs:
@ -117,20 +117,19 @@ def space_overview(request):
allow_sharing=settings.SPACE_DEFAULT_ALLOW_SHARING, allow_sharing=settings.SPACE_DEFAULT_ALLOW_SHARING,
) )
user_space = UserSpace.objects.create(space=created_space, user=request.user, active=True) user_space = UserSpace.objects.create(space=created_space, user=request.user, active=False)
user_space.groups.add(Group.objects.filter(name='admin').get()) user_space.groups.add(Group.objects.filter(name='admin').get())
messages.add_message(request, messages.SUCCESS, messages.add_message(request, messages.SUCCESS,
_('You have successfully created your own recipe space. Start by adding some recipes or invite other people to join you.')) _('You have successfully created your own recipe space. Start by adding some recipes or invite other people to join you.'))
return HttpResponseRedirect(reverse('view_switch_space', args=[user_space.pk])) return HttpResponseRedirect(reverse('view_switch_space', args=[user_space.space.pk]))
if join_form.is_valid(): if join_form.is_valid():
return HttpResponseRedirect(reverse('view_invite', args=[join_form.cleaned_data['token']])) return HttpResponseRedirect(reverse('view_invite', args=[join_form.cleaned_data['token']]))
else: else:
if settings.SOCIAL_DEFAULT_ACCESS: if settings.SOCIAL_DEFAULT_ACCESS:
request.user.userpreference.space = Space.objects.first() user_space = UserSpace.objects.create(space=Space.objects.first(), user=request.user, active=True)
request.user.userpreference.save() user_space.groups.add(Group.objects.filter(name=settings.SOCIAL_DEFAULT_GROUP).get())
request.user.groups.add(Group.objects.get(name=settings.SOCIAL_DEFAULT_GROUP))
return HttpResponseRedirect(reverse('index')) return HttpResponseRedirect(reverse('index'))
if 'signup_token' in request.session: if 'signup_token' in request.session:
return HttpResponseRedirect(reverse('view_invite', args=[request.session.pop('signup_token', '')])) return HttpResponseRedirect(reverse('view_invite', args=[request.session.pop('signup_token', '')]))
@ -476,14 +475,6 @@ def setup(request):
user.set_password(form.cleaned_data['password']) user.set_password(form.cleaned_data['password'])
user.save() user.save()
user.groups.add(Group.objects.get(name='admin'))
user.userpreference.space = Space.objects.first()
user.userpreference.save()
for x in Space.objects.all():
x.created_by = user
x.save()
messages.add_message(request, messages.SUCCESS, _('User has been created, please login!')) messages.add_message(request, messages.SUCCESS, _('User has been created, please login!'))
return HttpResponseRedirect(reverse('account_login')) return HttpResponseRedirect(reverse('account_login'))
except ValidationError as e: except ValidationError as e:
@ -504,7 +495,7 @@ def invite_link(request, token):
return HttpResponseRedirect(reverse('index')) return HttpResponseRedirect(reverse('index'))
if link := InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None, uuid=token).first(): if link := InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None, uuid=token).first():
if request.user.is_authenticated: if request.user.is_authenticated and not request.user.userspace_set.filter(space=link.space).exists():
link.used_by = request.user link.used_by = request.user
link.save() link.save()
@ -526,85 +517,12 @@ def invite_link(request, token):
return HttpResponseRedirect(reverse('view_space_overview')) return HttpResponseRedirect(reverse('view_space_overview'))
# TODO deprecated with 0.16.2 remove at some point
def signup(request, token):
return HttpResponseRedirect(reverse('view_invite', args=[token]))
@group_required('admin') @group_required('admin')
def space_manage(request, space_id): def space_manage(request, space_id):
user_space = get_object_or_404(UserSpace, space=space_id, user=request.user) user_space = get_object_or_404(UserSpace, space=space_id, user=request.user)
switch_user_active_space(request.user, user_space) switch_user_active_space(request.user, user_space)
return render(request, 'space_manage.html', {}) return render(request, 'space_manage.html', {})
@group_required('admin')
def space(request):
space_users = UserSpace.objects.filter(space=request.space).all()
counts = Object()
counts.recipes = Recipe.objects.filter(space=request.space).count()
counts.keywords = Keyword.objects.filter(space=request.space).count()
counts.recipe_import = RecipeImport.objects.filter(space=request.space).count()
counts.units = Unit.objects.filter(space=request.space).count()
counts.ingredients = Food.objects.filter(space=request.space).count()
counts.comments = Comment.objects.filter(recipe__space=request.space).count()
counts.recipes_internal = Recipe.objects.filter(internal=True, space=request.space).count()
counts.recipes_external = counts.recipes - counts.recipes_internal
counts.recipes_no_keyword = Recipe.objects.filter(keywords=None, space=request.space).count()
invite_links = InviteLinkTable(
InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None, space=request.space).all())
RequestConfig(request, paginate={'per_page': 25}).configure(invite_links)
space_form = SpacePreferenceForm(instance=request.space)
space_form.base_fields['food_inherit'].queryset = Food.inheritable_fields
if request.method == "POST" and 'space_form' in request.POST:
form = SpacePreferenceForm(request.POST, prefix='space')
if form.is_valid():
request.space.food_inherit.set(form.cleaned_data['food_inherit'])
request.space.show_facet_count = form.cleaned_data['show_facet_count']
request.space.save()
if form.cleaned_data['reset_food_inherit']:
Food.reset_inheritance(space=request.space)
return render(request, 'space.html', {
'space_users': space_users,
'counts': counts,
'invite_links': invite_links,
'space_form': space_form
})
# TODO super hacky and quick solution, safe but needs rework
# TODO move group settings to space to prevent permissions from one space to move to another
@group_required('admin')
def space_change_member(request, user_id, space_id, group):
m_space = get_object_or_404(Space, pk=space_id)
m_user = get_object_or_404(User, pk=user_id)
if request.user == m_space.created_by and m_user != m_space.created_by:
if m_user.userpreference.space == m_space:
if group == 'admin':
m_user.groups.clear()
m_user.groups.add(Group.objects.get(name='admin'))
return HttpResponseRedirect(reverse('view_space'))
if group == 'user':
m_user.groups.clear()
m_user.groups.add(Group.objects.get(name='user'))
return HttpResponseRedirect(reverse('view_space'))
if group == 'guest':
m_user.groups.clear()
m_user.groups.add(Group.objects.get(name='guest'))
return HttpResponseRedirect(reverse('view_space'))
if group == 'remove':
m_user.groups.clear()
m_user.userpreference.space = None
m_user.userpreference.save()
return HttpResponseRedirect(reverse('view_space'))
return HttpResponseRedirect(reverse('view_space'))
def report_share_abuse(request, token): def report_share_abuse(request, token):
if not settings.SHARING_ABUSE: if not settings.SHARING_ABUSE:

View File

@ -6,7 +6,7 @@
<div v-if="space !== undefined"> <div v-if="space !== undefined">
<h6><i class="fas fa-book"></i> {{ $t('Recipes') }}</h6> <h6><i class="fas fa-book"></i> {{ $t('Recipes') }}</h6>
<b-progress height="1.5rem" :max="space.max_recipes" variant="success" :striped="true"> <b-progress height="1.5rem" :max="space.max_recipes" variant="success" :striped="true">
<b-progress-bar :value="space.recipe_count"> <b-progress-bar :value="space.recipe_count" class="text-dark font-weight-bold">
{{ space.recipe_count }} / {{ space.recipe_count }} /
<template v-if="space.max_recipes === 0"></template> <template v-if="space.max_recipes === 0"></template>
<template v-else>{{ space.max_recipes }}</template> <template v-else>{{ space.max_recipes }}</template>
@ -15,7 +15,7 @@
<h6 class="mt-2"><i class="fas fa-users"></i> {{ $t('Users') }}</h6> <h6 class="mt-2"><i class="fas fa-users"></i> {{ $t('Users') }}</h6>
<b-progress height="1.5rem" :max="space.max_users" variant="success" :striped="true"> <b-progress height="1.5rem" :max="space.max_users" variant="success" :striped="true">
<b-progress-bar :value="space.user_count"> <b-progress-bar :value="space.user_count" class="text-dark font-weight-bold">
{{ space.user_count }} / {{ space.user_count }} /
<template v-if="space.max_users === 0"></template> <template v-if="space.max_users === 0"></template>
<template v-else>{{ space.max_users }}</template> <template v-else>{{ space.max_users }}</template>
@ -24,7 +24,7 @@
<h6 class="mt-2"><i class="fas fa-file"></i> {{ $t('Files') }}</h6> <h6 class="mt-2"><i class="fas fa-file"></i> {{ $t('Files') }}</h6>
<b-progress height="1.5rem" :max="space.max_file_storage_mb" variant="success" :striped="true"> <b-progress height="1.5rem" :max="space.max_file_storage_mb" variant="success" :striped="true">
<b-progress-bar :value="space.file_size_mb"> <b-progress-bar :value="space.file_size_mb" class="text-dark font-weight-bold">
{{ space.file_size_mb }} / {{ space.file_size_mb }} /
<template v-if="space.max_file_storage_mb === 0"></template> <template v-if="space.max_file_storage_mb === 0"></template>
<template v-else>{{ space.max_file_storage_mb }}</template> <template v-else>{{ space.max_file_storage_mb }}</template>
@ -85,7 +85,7 @@
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
<tr v-for="il in invite_links" :key="il.id"> <tr v-for="il in active_invite_links" :key="il.id">
<td>{{ il.id }}</td> <td>{{ il.id }}</td>
<td>{{ il.email }}</td> <td>{{ il.email }}</td>
<td> <td>
@ -134,6 +134,44 @@
</div> </div>
</div> </div>
<div class="row mt-4" v-if="space !== undefined">
<div class="col col-12">
<h4 class="mt-2"><i class="fas fa-cogs"></i> {{ $t('Settings') }}</h4>
<label>{{ $t('Message') }}</label>
<b-form-textarea v-model="space.message"></b-form-textarea>
<b-form-checkbox v-model="space.show_facet_count"> Facet Count</b-form-checkbox>
<span class="text-muted small">{{ $t('facet_count_info') }}</span><br/>
<label>{{ $t('FoodInherit') }}</label>
<generic-multiselect :initial_selection="space.food_inherit"
:model="Models.FOOD_INHERIT_FIELDS"
@change="space.food_inherit = $event.val;">
</generic-multiselect>
<span class="text-muted small">{{ $t('food_inherit_info') }}</span><br/>
<a class="btn btn-success" @click="updateSpace()">{{ $t('Update') }}</a><br/>
<a class="btn btn-warning mt-1" @click="resetInheritance()">{{ $t('reset_food_inheritance') }}</a><br/>
<span class="text-muted small">{{ $t('reset_food_inheritance_info') }}</span>
</div>
</div>
<div class="row mt-4">
<div class="col col-12">
<h4 class="mt-2"><i class="fas fa-trash"></i> {{ $t('Delete') }}</h4>
{{ $t('warning_space_delete') }}
<br/>
<a class="btn btn-danger" :href="resolveDjangoUrl('delete_space', ACTIVE_SPACE_ID)">{{
$t('Delete')
}}</a>
</div>
</div>
<br/>
<br/>
<generic-modal-form :model="Models.INVITE_LINK" :action="Actions.CREATE" :show="show_invite_create" <generic-modal-form :model="Models.INVITE_LINK" :action="Actions.CREATE" :show="show_invite_create"
@finish-action="show_invite_create = false; loadInviteLinks()"/> @finish-action="show_invite_create = false; loadInviteLinks()"/>
@ -146,11 +184,12 @@ import {BootstrapVue} from "bootstrap-vue"
import "bootstrap-vue/dist/bootstrap-vue.css" import "bootstrap-vue/dist/bootstrap-vue.css"
import {ApiMixin, ResolveUrlMixin, StandardToasts, ToastMixin} from "@/utils/utils" import {ApiMixin, resolveDjangoUrl, ResolveUrlMixin, StandardToasts, ToastMixin} from "@/utils/utils"
import {ApiApiFactory} from "@/utils/openapi/api.ts" import {ApiApiFactory} from "@/utils/openapi/api.ts"
import GenericMultiselect from "@/components/GenericMultiselect"; import GenericMultiselect from "@/components/GenericMultiselect";
import GenericModalForm from "@/components/Modals/GenericModalForm"; import GenericModalForm from "@/components/Modals/GenericModalForm";
import axios from "axios";
Vue.use(BootstrapVue) Vue.use(BootstrapVue)
@ -160,17 +199,23 @@ export default {
components: {GenericMultiselect, GenericModalForm}, components: {GenericMultiselect, GenericModalForm},
data() { data() {
return { return {
ACTIVE_SPACE_ID: window.ACTIVE_SPACE_ID,
space: undefined, space: undefined,
user_spaces: [], user_spaces: [],
invite_links: [], invite_links: [],
show_invite_create: false show_invite_create: false
} }
}, },
computed: {
active_invite_links: function () {
return this.invite_links.filter(il => il.used_by === null)
},
},
mounted() { mounted() {
this.$i18n.locale = window.CUSTOM_LOCALE this.$i18n.locale = window.CUSTOM_LOCALE
let apiFactory = new ApiApiFactory() let apiFactory = new ApiApiFactory()
apiFactory.retrieveSpace(window.ACTIVE_SPACE_ID).then(r => { apiFactory.retrieveSpace(this.ACTIVE_SPACE_ID).then(r => {
this.space = r.data this.space = r.data
}) })
apiFactory.listUserSpaces().then(r => { apiFactory.listUserSpaces().then(r => {
@ -192,6 +237,14 @@ export default {
this.invite_links = r.data this.invite_links = r.data
}) })
}, },
updateSpace: function () {
let apiFactory = new ApiApiFactory()
apiFactory.partialUpdateSpace(this.ACTIVE_SPACE_ID, this.space).then(r => {
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
},
updateUserSpace: function (userSpace) { updateUserSpace: function (userSpace) {
let apiFactory = new ApiApiFactory() let apiFactory = new ApiApiFactory()
apiFactory.partialUpdateUserSpace(userSpace.id, userSpace).then(r => { apiFactory.partialUpdateUserSpace(userSpace.id, userSpace).then(r => {
@ -219,7 +272,13 @@ export default {
}).catch(err => { }).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_DELETE, err) StandardToasts.makeStandardToast(this, StandardToasts.FAIL_DELETE, err)
}) })
},
resetInheritance: function () {
axios.get(resolveDjangoUrl('api_reset_food_inheritance')).then(r => {
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
}, },
}, },
} }

View File

@ -14,6 +14,9 @@
"success_moving_resource": "Successfully moved a resource!", "success_moving_resource": "Successfully moved a resource!",
"success_merging_resource": "Successfully merged a resource!", "success_merging_resource": "Successfully merged a resource!",
"file_upload_disabled": "File upload is not enabled for your space.", "file_upload_disabled": "File upload is not enabled for your space.",
"warning_space_delete": "You can delete your space including all recipes, shopping lists, meal plans and whatever else you have created. This cannot be undone! Are you sure you want to do this ?",
"food_inherit_info": "Fields on food that should be inherited by default.",
"facet_count_info": "Show recipe counts on search filters.",
"step_time_minutes": "Step time in minutes", "step_time_minutes": "Step time in minutes",
"confirm_delete": "Are you sure you want to delete this {object}?", "confirm_delete": "Are you sure you want to delete this {object}?",
"import_running": "Import running, please wait!", "import_running": "Import running, please wait!",
@ -344,6 +347,7 @@
"filter": "Filter", "filter": "Filter",
"Website": "Website", "Website": "Website",
"App": "App", "App": "App",
"Message": "Message",
"Bookmarklet": "Bookmarklet", "Bookmarklet": "Bookmarklet",
"click_image_import": "Click the image you want to import for this recipe", "click_image_import": "Click the image you want to import for this recipe",
"no_more_images_found": "No additional images found on Website.", "no_more_images_found": "No additional images found on Website.",
@ -355,7 +359,9 @@
"search_create_help_text": "Create a new recipe directly in Tandoor.", "search_create_help_text": "Create a new recipe directly in Tandoor.",
"warning_duplicate_filter": "Warning: Due to technical limitations having multiple filters of the same combination (and/or/not) might yield unexpected results.", "warning_duplicate_filter": "Warning: Due to technical limitations having multiple filters of the same combination (and/or/not) might yield unexpected results.",
"reset_children": "Reset Child Inheritance", "reset_children": "Reset Child Inheritance",
"reset_children_help": "Overwrite all children with values from inherited fields. Inheritted fields of children will be set to Inherit Fields unless Children Inherit Fields is set.", "reset_children_help": "Overwrite all children with values from inherited fields. Inherited fields of children will be set to Inherit Fields unless Children Inherit Fields is set.",
"reset_food_inheritance": "Reset Inheritance",
"reset_food_inheritance_info": "Reset all foods to default inherited fields and their parent values.",
"substitute_help": "Substitutes are considered when searching for recipes that can be made with onhand ingredients.", "substitute_help": "Substitutes are considered when searching for recipes that can be made with onhand ingredients.",
"substitute_siblings_help": "All food that share a parent of this food are considered substitutes.", "substitute_siblings_help": "All food that share a parent of this food are considered substitutes.",
"substitute_children_help": "All food that are children of this food are considered substitutes.", "substitute_children_help": "All food that are children of this food are considered substitutes.",
@ -364,7 +370,7 @@
"SubstituteOnHand": "You have a substitute on hand.", "SubstituteOnHand": "You have a substitute on hand.",
"ChildInheritFields": "Children Inherit Fields", "ChildInheritFields": "Children Inherit Fields",
"ChildInheritFields_help": "Children will inherit these fields by default.", "ChildInheritFields_help": "Children will inherit these fields by default.",
"InheritFields_help": "The values of these fields will be inheritted from parent (Exception: blank shopping categories are not inheritted)", "InheritFields_help": "The values of these fields will be inherited from parent (Exception: blank shopping categories are not inherited)",
"last_viewed": "Last Viewed", "last_viewed": "Last Viewed",
"created_on": "Created On", "created_on": "Created On",
"updatedon": "Updated On", "updatedon": "Updated On",