lots of fixes and stuff
This commit is contained in:
parent
07f78bb7b8
commit
e2b887b449
@ -32,41 +32,7 @@ admin.site.unregister(Group)
|
||||
@admin.action(description='Delete all data from a space')
|
||||
def delete_space_action(modeladmin, request, queryset):
|
||||
for space in queryset:
|
||||
CookLog.objects.filter(space=space).delete()
|
||||
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()
|
||||
space.save()
|
||||
|
||||
|
||||
class SpaceAdmin(admin.ModelAdmin):
|
||||
|
@ -760,6 +760,6 @@ def old_search(request):
|
||||
params = dict(request.GET)
|
||||
params['internal'] = None
|
||||
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)
|
||||
return f.qs
|
||||
|
@ -4,11 +4,12 @@ from django.db import migrations
|
||||
|
||||
|
||||
def create_default_space(apps, schema_editor):
|
||||
Space = apps.get_model('cookbook', 'Space')
|
||||
Space.objects.create(
|
||||
name='Default',
|
||||
message=''
|
||||
)
|
||||
# Space = apps.get_model('cookbook', 'Space')
|
||||
# Space.objects.create(
|
||||
# name='Default',
|
||||
# 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):
|
||||
|
@ -240,7 +240,49 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
|
||||
demo = models.BooleanField(default=False)
|
||||
food_inherit = models.ManyToManyField(FoodInheritField, blank=True)
|
||||
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):
|
||||
return self.created_by
|
||||
|
||||
@ -551,14 +593,14 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
|
||||
tree_filter = Q(space=space)
|
||||
|
||||
# remove all inherited fields from food
|
||||
Through = Food.objects.filter(tree_filter).first().inherit_fields.through
|
||||
Through.objects.all().delete()
|
||||
trough = Food.objects.filter(tree_filter).first().inherit_fields.through
|
||||
trough.objects.all().delete()
|
||||
# food is going to inherit attributes
|
||||
if len(inherit) > 0:
|
||||
# ManyToMany cannot be updated through an UPDATE operation
|
||||
for i in inherit:
|
||||
Through.objects.bulk_create([
|
||||
Through(food_id=x, foodinheritfield_id=i['id'])
|
||||
trough.objects.bulk_create([
|
||||
trough(food_id=x, foodinheritfield_id=i['id'])
|
||||
for x in Food.objects.filter(tree_filter).values_list('id', flat=True)
|
||||
])
|
||||
|
||||
|
@ -151,10 +151,27 @@ class GroupSerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
|
||||
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')
|
||||
recipe_count = serializers.SerializerMethodField('get_recipe_count')
|
||||
file_size_mb = serializers.SerializerMethodField('get_file_size_mb')
|
||||
food_inherit = FoodInheritFieldSerializer(many=True)
|
||||
|
||||
def get_user_count(self, obj):
|
||||
return UserSpace.objects.filter(space=obj).count()
|
||||
@ -174,13 +191,18 @@ class SpaceSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
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',)
|
||||
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):
|
||||
user = UserNameSerializer(read_only=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):
|
||||
raise ValidationError('Cannot create using this endpoint')
|
||||
|
||||
@ -209,24 +231,6 @@ class MealTypeSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
|
||||
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):
|
||||
food_inherit_default = FoodInheritFieldSerializer(source='space.food_inherit', many=True, allow_null=True,
|
||||
required=False, read_only=True)
|
||||
@ -1073,7 +1077,7 @@ class InviteLinkSerializer(WritableNestedModelSerializer):
|
||||
class BookmarkletImportListSerializer(serializers.ModelSerializer):
|
||||
def create(self, validated_data):
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
|
@ -2,6 +2,7 @@ from decimal import Decimal
|
||||
from functools import wraps
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.postgres.search import SearchVector
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
@ -11,7 +12,7 @@ from django_scopes import scope
|
||||
from cookbook.helper.shopping_helper import RecipeShoppingEditor
|
||||
from cookbook.managers import DICTIONARY
|
||||
from cookbook.models import (Food, FoodInheritField, Ingredient, MealPlan, Recipe,
|
||||
ShoppingListEntry, Step)
|
||||
ShoppingListEntry, Step, UserPreference)
|
||||
|
||||
SQLITE = True
|
||||
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'):
|
||||
return None
|
||||
return signal_func(sender, instance, **kwargs)
|
||||
|
||||
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)
|
||||
@skip_signal
|
||||
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")
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
|
||||
|
@ -294,7 +294,7 @@
|
||||
<a class="dropdown-item" href="{% url 'data_sync' %}"><i
|
||||
class="fas fa-sync-alt fa-fw"></i> {% trans 'External Recipes' %}</a>
|
||||
{% 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>
|
||||
{% endif %}
|
||||
{% if user.is_superuser %}
|
||||
@ -316,7 +316,7 @@
|
||||
{% endif %}
|
||||
{{ us.space.name }}</a>
|
||||
{% 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 %}
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="{% url 'docs_markdown' %}"><i
|
||||
@ -344,7 +344,7 @@
|
||||
</div>
|
||||
</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 %}
|
||||
<div class="bg-success" style=" width: 100%; text-align: center!important; color: #ffffff; padding: 8px">
|
||||
{{ message_of_the_day }}
|
||||
|
@ -17,7 +17,7 @@
|
||||
|
||||
<nav aria-label="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>
|
||||
</nav>
|
||||
|
||||
|
@ -12,7 +12,7 @@
|
||||
<div class="col col-12">
|
||||
<nav aria-label="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>
|
||||
</nav>
|
||||
</div>
|
||||
|
@ -3,7 +3,7 @@
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "No Space" %}{% endblock %}
|
||||
{% block title %}{% trans "Overview" %}{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
@ -36,6 +36,11 @@
|
||||
<h5 class="card-title"><a
|
||||
href="{% url 'view_switch_space' us.space.id %}">{{ us.space.name }}</a>
|
||||
</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
|
||||
class="text-muted">{% trans 'Owner' %}: {{ us.space.created_by }}</small>
|
||||
{% if us.space.created_by != us.user %}
|
||||
|
@ -111,8 +111,12 @@ def page_help(page_name):
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def message_of_the_day():
|
||||
return Space.objects.first().message
|
||||
def message_of_the_day(request):
|
||||
try:
|
||||
if request.space.message:
|
||||
return request.space.message
|
||||
except (AttributeError, KeyError, ValueError):
|
||||
pass
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
|
@ -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}, 'steps__recipe_count': 1}), # shopping list from recipe with StepRecipe and food 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():
|
||||
recipe.space = auth.get_user(u1_s2).userpreference.space
|
||||
recipe.space = space_2
|
||||
recipe.save()
|
||||
assert len(json.loads(
|
||||
u1_s2.get(
|
||||
|
@ -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]
|
||||
|
||||
|
||||
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():
|
||||
user1 = auth.get_user(u1_s1)
|
||||
user2 = auth.get_user(u2_s1)
|
||||
space = user1.userpreference.space
|
||||
space = space_1
|
||||
user3 = UserFactory(space=space)
|
||||
recipe1 = RecipeFactory(created_by=user1, space=space)
|
||||
recipe2 = RecipeFactory(created_by=user2, space=space)
|
||||
|
@ -12,7 +12,7 @@ from recipes.version import VERSION_NUMBER
|
||||
from .models import (Automation, Comment, CustomFilter, Food, InviteLink, Keyword, MealPlan, Recipe,
|
||||
RecipeBook, RecipeBookEntry, RecipeImport, ShoppingList, Step, Storage,
|
||||
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.api import CustomAuthToken
|
||||
|
||||
@ -55,15 +55,11 @@ router.register(r'view-log', api.ViewLogViewSet)
|
||||
urlpatterns = [
|
||||
path('', views.index, name='index'),
|
||||
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('space-overview', views.space_overview, name='view_space_overview'),
|
||||
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('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('system/', views.system, name='view_system'),
|
||||
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/share-link/<int:pk>', api.share_link, name='api_share_link'),
|
||||
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'),
|
||||
# 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 = (
|
||||
Recipe, RecipeImport, Storage, RecipeBook, MealPlan, SyncLog, Sync,
|
||||
Comment, RecipeBookEntry, ShoppingList, InviteLink, UserSpace
|
||||
Comment, RecipeBookEntry, ShoppingList, InviteLink, UserSpace, Space
|
||||
)
|
||||
|
||||
for m in generic_models:
|
||||
|
@ -2,6 +2,7 @@ import io
|
||||
import json
|
||||
import mimetypes
|
||||
import re
|
||||
import traceback
|
||||
import uuid
|
||||
from collections import OrderedDict
|
||||
|
||||
@ -172,9 +173,9 @@ class FuzzyFilterMixin(ViewSetMixin, ExtendedRecipeMixin):
|
||||
|
||||
self.queryset = (
|
||||
self.queryset
|
||||
.annotate(starts=Case(When(name__istartswith=query, then=(Value(100))),
|
||||
default=Value(0))) # put exact matches at the top of the result set
|
||||
.filter(filter).order_by('-starts', Lower('name').asc())
|
||||
.annotate(starts=Case(When(name__istartswith=query, then=(Value(100))),
|
||||
default=Value(0))) # put exact matches at the top of the result set
|
||||
.filter(filter).order_by('-starts', Lower('name').asc())
|
||||
)
|
||||
|
||||
updated_at = self.request.query_params.get('updated_at', None)
|
||||
@ -388,6 +389,11 @@ class UserSpaceViewSet(viewsets.ModelViewSet):
|
||||
permission_classes = [CustomIsSpaceOwner]
|
||||
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):
|
||||
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)
|
||||
|
||||
|
||||
@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):
|
||||
if recipe.storage.method == Storage.DROPBOX:
|
||||
return Dropbox
|
||||
|
@ -9,7 +9,7 @@ from django.views.generic import DeleteView
|
||||
|
||||
from cookbook.helper.permission_helper import GroupRequiredMixin, OwnerRequiredMixin, group_required
|
||||
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.local import Local
|
||||
from cookbook.provider.nextcloud import Nextcloud
|
||||
@ -199,3 +199,19 @@ class UserSpaceDelete(OwnerRequiredMixin, DeleteView):
|
||||
context = super(UserSpaceDelete, self).get_context_data(**kwargs)
|
||||
context['title'] = _("Space Membership")
|
||||
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
|
||||
|
@ -61,7 +61,7 @@ def search(request):
|
||||
if request.user.userpreference.search_style == UserPreference.NEW:
|
||||
return search_v2(request)
|
||||
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()),
|
||||
space=request.space)
|
||||
if request.user.userpreference.search_style == UserPreference.LARGE:
|
||||
@ -72,7 +72,7 @@ def search(request):
|
||||
|
||||
if request.GET == {} and request.user.userpreference.show_recent:
|
||||
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 = []
|
||||
for r in qs:
|
||||
@ -117,20 +117,19 @@ def space_overview(request):
|
||||
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())
|
||||
|
||||
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.'))
|
||||
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():
|
||||
return HttpResponseRedirect(reverse('view_invite', args=[join_form.cleaned_data['token']]))
|
||||
else:
|
||||
if settings.SOCIAL_DEFAULT_ACCESS:
|
||||
request.user.userpreference.space = Space.objects.first()
|
||||
request.user.userpreference.save()
|
||||
request.user.groups.add(Group.objects.get(name=settings.SOCIAL_DEFAULT_GROUP))
|
||||
user_space = UserSpace.objects.create(space=Space.objects.first(), user=request.user, active=True)
|
||||
user_space.groups.add(Group.objects.filter(name=settings.SOCIAL_DEFAULT_GROUP).get())
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
if 'signup_token' in request.session:
|
||||
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.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!'))
|
||||
return HttpResponseRedirect(reverse('account_login'))
|
||||
except ValidationError as e:
|
||||
@ -504,7 +495,7 @@ def invite_link(request, token):
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
|
||||
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.save()
|
||||
|
||||
@ -526,85 +517,12 @@ def invite_link(request, token):
|
||||
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')
|
||||
def space_manage(request, space_id):
|
||||
user_space = get_object_or_404(UserSpace, space=space_id, user=request.user)
|
||||
switch_user_active_space(request.user, user_space)
|
||||
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):
|
||||
if not settings.SHARING_ABUSE:
|
||||
|
@ -6,7 +6,7 @@
|
||||
<div v-if="space !== undefined">
|
||||
<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-bar :value="space.recipe_count">
|
||||
<b-progress-bar :value="space.recipe_count" class="text-dark font-weight-bold">
|
||||
{{ space.recipe_count }} /
|
||||
<template v-if="space.max_recipes === 0">∞</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>
|
||||
<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 }} /
|
||||
<template v-if="space.max_users === 0">∞</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>
|
||||
<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 }} /
|
||||
<template v-if="space.max_file_storage_mb === 0">∞</template>
|
||||
<template v-else>{{ space.max_file_storage_mb }}</template>
|
||||
@ -85,7 +85,7 @@
|
||||
<th></th>
|
||||
</tr>
|
||||
</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.email }}</td>
|
||||
<td>
|
||||
@ -134,6 +134,44 @@
|
||||
</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"
|
||||
@finish-action="show_invite_create = false; loadInviteLinks()"/>
|
||||
|
||||
@ -146,11 +184,12 @@ import {BootstrapVue} from "bootstrap-vue"
|
||||
|
||||
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 GenericMultiselect from "@/components/GenericMultiselect";
|
||||
import GenericModalForm from "@/components/Modals/GenericModalForm";
|
||||
import axios from "axios";
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
@ -160,17 +199,23 @@ export default {
|
||||
components: {GenericMultiselect, GenericModalForm},
|
||||
data() {
|
||||
return {
|
||||
ACTIVE_SPACE_ID: window.ACTIVE_SPACE_ID,
|
||||
space: undefined,
|
||||
user_spaces: [],
|
||||
invite_links: [],
|
||||
show_invite_create: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
active_invite_links: function () {
|
||||
return this.invite_links.filter(il => il.used_by === null)
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$i18n.locale = window.CUSTOM_LOCALE
|
||||
|
||||
let apiFactory = new ApiApiFactory()
|
||||
apiFactory.retrieveSpace(window.ACTIVE_SPACE_ID).then(r => {
|
||||
apiFactory.retrieveSpace(this.ACTIVE_SPACE_ID).then(r => {
|
||||
this.space = r.data
|
||||
})
|
||||
apiFactory.listUserSpaces().then(r => {
|
||||
@ -192,6 +237,14 @@ export default {
|
||||
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) {
|
||||
let apiFactory = new ApiApiFactory()
|
||||
apiFactory.partialUpdateUserSpace(userSpace.id, userSpace).then(r => {
|
||||
@ -219,7 +272,13 @@ export default {
|
||||
}).catch(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)
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -14,6 +14,9 @@
|
||||
"success_moving_resource": "Successfully moved a resource!",
|
||||
"success_merging_resource": "Successfully merged a resource!",
|
||||
"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",
|
||||
"confirm_delete": "Are you sure you want to delete this {object}?",
|
||||
"import_running": "Import running, please wait!",
|
||||
@ -344,6 +347,7 @@
|
||||
"filter": "Filter",
|
||||
"Website": "Website",
|
||||
"App": "App",
|
||||
"Message": "Message",
|
||||
"Bookmarklet": "Bookmarklet",
|
||||
"click_image_import": "Click the image you want to import for this recipe",
|
||||
"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.",
|
||||
"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_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_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.",
|
||||
@ -364,7 +370,7 @@
|
||||
"SubstituteOnHand": "You have a substitute on hand.",
|
||||
"ChildInheritFields": "Children Inherit Fields",
|
||||
"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",
|
||||
"created_on": "Created On",
|
||||
"updatedon": "Updated On",
|
||||
|
Loading…
Reference in New Issue
Block a user