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')
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):

View File

@ -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

View File

@ -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):

View File

@ -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)
])

View File

@ -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:

View File

@ -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

View File

@ -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 }}

View File

@ -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>

View File

@ -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>

View File

@ -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 %}

View File

@ -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

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}, '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(

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]
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)

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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)
})
},
},
}

View File

@ -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",