add missing from rebase
This commit is contained in:
parent
e5f0c19cdc
commit
bf0462cd74
@ -76,7 +76,7 @@ from cookbook.models import (Automation, BookmarkletImport, CookLog, CustomFilte
|
|||||||
ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
|
ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
|
||||||
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync,
|
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync,
|
||||||
SyncLog, Unit, UnitConversion, UserFile, UserPreference, UserSpace,
|
SyncLog, Unit, UnitConversion, UserFile, UserPreference, UserSpace,
|
||||||
ViewLog)
|
ViewLog, HomeAssistantConfig)
|
||||||
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
|
||||||
@ -102,7 +102,7 @@ from cookbook.serializer import (AccessTokenSerializer, AutomationSerializer,
|
|||||||
SupermarketCategorySerializer, SupermarketSerializer,
|
SupermarketCategorySerializer, SupermarketSerializer,
|
||||||
SyncLogSerializer, SyncSerializer, UnitConversionSerializer,
|
SyncLogSerializer, SyncSerializer, UnitConversionSerializer,
|
||||||
UnitSerializer, UserFileSerializer, UserPreferenceSerializer,
|
UnitSerializer, UserFileSerializer, UserPreferenceSerializer,
|
||||||
UserSerializer, UserSpaceSerializer, ViewLogSerializer)
|
UserSerializer, UserSpaceSerializer, ViewLogSerializer, HomeAssistantConfigSerializer)
|
||||||
from cookbook.views.import_export import get_integration
|
from cookbook.views.import_export import get_integration
|
||||||
from recipes import settings
|
from recipes import settings
|
||||||
from recipes.settings import FDC_API_KEY, DRF_THROTTLE_RECIPE_URL_IMPORT
|
from recipes.settings import FDC_API_KEY, DRF_THROTTLE_RECIPE_URL_IMPORT
|
||||||
@ -181,7 +181,8 @@ class FuzzyFilterMixin(ViewSetMixin, ExtendedRecipeMixin):
|
|||||||
self.queryset = self.queryset.filter(space=self.request.space).order_by(Lower('name').asc())
|
self.queryset = self.queryset.filter(space=self.request.space).order_by(Lower('name').asc())
|
||||||
query = self.request.query_params.get('query', None)
|
query = self.request.query_params.get('query', None)
|
||||||
if self.request.user.is_authenticated:
|
if self.request.user.is_authenticated:
|
||||||
fuzzy = self.request.user.searchpreference.lookup or any([self.model.__name__.lower() in x for x in
|
fuzzy = self.request.user.searchpreference.lookup or any(
|
||||||
|
[self.model.__name__.lower() in x for x in
|
||||||
self.request.user.searchpreference.trigram.values_list(
|
self.request.user.searchpreference.trigram.values_list(
|
||||||
'field', flat=True)])
|
'field', flat=True)])
|
||||||
else:
|
else:
|
||||||
@ -203,7 +204,9 @@ class FuzzyFilterMixin(ViewSetMixin, ExtendedRecipeMixin):
|
|||||||
filter |= Q(name__unaccent__icontains=query)
|
filter |= Q(name__unaccent__icontains=query)
|
||||||
|
|
||||||
self.queryset = (
|
self.queryset = (
|
||||||
self.queryset.annotate(starts=Case(When(name__istartswith=query, then=(Value(100))),
|
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
|
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())
|
||||||
)
|
)
|
||||||
@ -326,7 +329,8 @@ class TreeMixin(MergeMixin, FuzzyFilterMixin, ExtendedRecipeMixin):
|
|||||||
return self.annotate_recipe(queryset=super().get_queryset(), request=self.request, serializer=self.serializer_class, tree=True)
|
return self.annotate_recipe(queryset=super().get_queryset(), request=self.request, serializer=self.serializer_class, tree=True)
|
||||||
self.queryset = self.queryset.filter(space=self.request.space).order_by(Lower('name').asc())
|
self.queryset = self.queryset.filter(space=self.request.space).order_by(Lower('name').asc())
|
||||||
|
|
||||||
return self.annotate_recipe(queryset=self.queryset, request=self.request, serializer=self.serializer_class,
|
return self.annotate_recipe(
|
||||||
|
queryset=self.queryset, request=self.request, serializer=self.serializer_class,
|
||||||
tree=True)
|
tree=True)
|
||||||
|
|
||||||
@decorators.action(detail=True, url_path='move/(?P<parent>[^/.]+)', methods=['PUT'], )
|
@decorators.action(detail=True, url_path='move/(?P<parent>[^/.]+)', methods=['PUT'], )
|
||||||
@ -572,7 +576,8 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
self.queryset = super().get_queryset()
|
self.queryset = super().get_queryset()
|
||||||
shopping_status = ShoppingListEntry.objects.filter(space=self.request.space, food=OuterRef('id'),
|
shopping_status = ShoppingListEntry.objects.filter(
|
||||||
|
space=self.request.space, food=OuterRef('id'),
|
||||||
checked=False).values('id')
|
checked=False).values('id')
|
||||||
# onhand_status = self.queryset.annotate(onhand_status=Exists(onhand_users_set__in=[shared_users]))
|
# onhand_status = self.queryset.annotate(onhand_status=Exists(onhand_users_set__in=[shared_users]))
|
||||||
return self.queryset \
|
return self.queryset \
|
||||||
@ -594,7 +599,8 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
|
|||||||
shared_users = list(self.request.user.get_shopping_share())
|
shared_users = list(self.request.user.get_shopping_share())
|
||||||
shared_users.append(request.user)
|
shared_users.append(request.user)
|
||||||
if request.data.get('_delete', False) == 'true':
|
if request.data.get('_delete', False) == 'true':
|
||||||
ShoppingListEntry.objects.filter(food=obj, checked=False, space=request.space,
|
ShoppingListEntry.objects.filter(
|
||||||
|
food=obj, checked=False, space=request.space,
|
||||||
created_by__in=shared_users).delete()
|
created_by__in=shared_users).delete()
|
||||||
content = {'msg': _(f'{obj.name} was removed from the shopping list.')}
|
content = {'msg': _(f'{obj.name} was removed from the shopping list.')}
|
||||||
return Response(content, status=status.HTTP_204_NO_CONTENT)
|
return Response(content, status=status.HTTP_204_NO_CONTENT)
|
||||||
@ -603,7 +609,8 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
|
|||||||
unit = request.data.get('unit', None)
|
unit = request.data.get('unit', None)
|
||||||
content = {'msg': _(f'{obj.name} was added to the shopping list.')}
|
content = {'msg': _(f'{obj.name} was added to the shopping list.')}
|
||||||
|
|
||||||
ShoppingListEntry.objects.create(food=obj, amount=amount, unit=unit, space=request.space,
|
ShoppingListEntry.objects.create(
|
||||||
|
food=obj, amount=amount, unit=unit, space=request.space,
|
||||||
created_by=request.user)
|
created_by=request.user)
|
||||||
return Response(content, status=status.HTTP_204_NO_CONTENT)
|
return Response(content, status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
@ -617,7 +624,10 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
|
|||||||
|
|
||||||
response = requests.get(f'https://api.nal.usda.gov/fdc/v1/food/{food.fdc_id}?api_key={FDC_API_KEY}')
|
response = requests.get(f'https://api.nal.usda.gov/fdc/v1/food/{food.fdc_id}?api_key={FDC_API_KEY}')
|
||||||
if response.status_code == 429:
|
if response.status_code == 429:
|
||||||
return JsonResponse({'msg', 'API Key Rate Limit reached/exceeded, see https://api.data.gov/docs/rate-limits/ for more information. Configure your key in Tandoor using environment FDC_API_KEY variable.'}, status=429,
|
return JsonResponse(
|
||||||
|
{'msg',
|
||||||
|
'API Key Rate Limit reached/exceeded, see https://api.data.gov/docs/rate-limits/ for more information. Configure your key in Tandoor using environment FDC_API_KEY variable.'},
|
||||||
|
status=429,
|
||||||
json_dumps_params={'indent': 4})
|
json_dumps_params={'indent': 4})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -634,7 +644,8 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
|
|||||||
if pt.fdc_id:
|
if pt.fdc_id:
|
||||||
for fn in data['foodNutrients']:
|
for fn in data['foodNutrients']:
|
||||||
if fn['nutrient']['id'] == pt.fdc_id:
|
if fn['nutrient']['id'] == pt.fdc_id:
|
||||||
food_property_list.append(Property(
|
food_property_list.append(
|
||||||
|
Property(
|
||||||
property_type_id=pt.id,
|
property_type_id=pt.id,
|
||||||
property_amount=round(fn['amount'], 2),
|
property_amount=round(fn['amount'], 2),
|
||||||
import_food_id=food.id,
|
import_food_id=food.id,
|
||||||
@ -874,7 +885,9 @@ class RecipePagination(PageNumberPagination):
|
|||||||
return super().paginate_queryset(queryset, request, view)
|
return super().paginate_queryset(queryset, request, view)
|
||||||
|
|
||||||
def get_paginated_response(self, data):
|
def get_paginated_response(self, data):
|
||||||
return Response(OrderedDict([
|
return Response(
|
||||||
|
OrderedDict(
|
||||||
|
[
|
||||||
('count', self.page.paginator.count),
|
('count', self.page.paginator.count),
|
||||||
('next', self.get_next_link()),
|
('next', self.get_next_link()),
|
||||||
('previous', self.get_previous_link()),
|
('previous', self.get_previous_link()),
|
||||||
@ -965,7 +978,8 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
def list(self, request, *args, **kwargs):
|
def list(self, request, *args, **kwargs):
|
||||||
if self.request.GET.get('debug', False):
|
if self.request.GET.get('debug', False):
|
||||||
return JsonResponse({
|
return JsonResponse(
|
||||||
|
{
|
||||||
'new': str(self.get_queryset().query),
|
'new': str(self.get_queryset().query),
|
||||||
})
|
})
|
||||||
return super().list(request, *args, **kwargs)
|
return super().list(request, *args, **kwargs)
|
||||||
@ -1137,7 +1151,9 @@ class ShoppingListEntryViewSet(viewsets.ModelViewSet):
|
|||||||
permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope]
|
permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope]
|
||||||
query_params = [
|
query_params = [
|
||||||
QueryParam(name='id', description=_('Returns the shopping list entry with a primary key of id. Multiple values allowed.'), qtype='int'),
|
QueryParam(name='id', description=_('Returns the shopping list entry with a primary key of id. Multiple values allowed.'), qtype='int'),
|
||||||
QueryParam(name='checked', description=_('Filter shopping list entries on checked. [''true'', ''false'', ''both'', ''<b>recent</b>'']<br> - ''recent'' includes unchecked items and recently completed items.')
|
QueryParam(
|
||||||
|
name='checked', description=_(
|
||||||
|
'Filter shopping list entries on checked. [''true'', ''false'', ''both'', ''<b>recent</b>'']<br> - ''recent'' includes unchecked items and recently completed items.')
|
||||||
),
|
),
|
||||||
QueryParam(name='supermarket', description=_('Returns the shopping list entries sorted by supermarket category order.'), qtype='int'),
|
QueryParam(name='supermarket', description=_('Returns the shopping list entries sorted by supermarket category order.'), qtype='int'),
|
||||||
]
|
]
|
||||||
@ -1329,7 +1345,8 @@ class CustomAuthToken(ObtainAuthToken):
|
|||||||
throttle_classes = [AuthTokenThrottle]
|
throttle_classes = [AuthTokenThrottle]
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
serializer = self.serializer_class(data=request.data,
|
serializer = self.serializer_class(
|
||||||
|
data=request.data,
|
||||||
context={'request': request})
|
context={'request': request})
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
user = serializer.validated_data['user']
|
user = serializer.validated_data['user']
|
||||||
@ -1337,10 +1354,12 @@ class CustomAuthToken(ObtainAuthToken):
|
|||||||
scope__contains='write').first():
|
scope__contains='write').first():
|
||||||
access_token = token
|
access_token = token
|
||||||
else:
|
else:
|
||||||
access_token = AccessToken.objects.create(user=user, token=f'tda_{str(uuid.uuid4()).replace("-", "_")}',
|
access_token = AccessToken.objects.create(
|
||||||
|
user=user, token=f'tda_{str(uuid.uuid4()).replace("-", "_")}',
|
||||||
expires=(timezone.now() + timezone.timedelta(days=365 * 5)),
|
expires=(timezone.now() + timezone.timedelta(days=365 * 5)),
|
||||||
scope='read write app')
|
scope='read write app')
|
||||||
return Response({
|
return Response(
|
||||||
|
{
|
||||||
'id': access_token.id,
|
'id': access_token.id,
|
||||||
'token': access_token.token,
|
'token': access_token.token,
|
||||||
'scope': access_token.scope,
|
'scope': access_token.scope,
|
||||||
@ -1376,7 +1395,8 @@ class RecipeUrlImportView(APIView):
|
|||||||
url = serializer.validated_data.get('url', None)
|
url = serializer.validated_data.get('url', None)
|
||||||
data = unquote(serializer.validated_data.get('data', None))
|
data = unquote(serializer.validated_data.get('data', None))
|
||||||
if not url and not data:
|
if not url and not data:
|
||||||
return Response({
|
return Response(
|
||||||
|
{
|
||||||
'error': True,
|
'error': True,
|
||||||
'msg': _('Nothing to do.')
|
'msg': _('Nothing to do.')
|
||||||
}, status=status.HTTP_400_BAD_REQUEST)
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
@ -1384,7 +1404,8 @@ class RecipeUrlImportView(APIView):
|
|||||||
elif url and not data:
|
elif url and not data:
|
||||||
if re.match('^(https?://)?(www\\.youtube\\.com|youtu\\.be)/.+$', url):
|
if re.match('^(https?://)?(www\\.youtube\\.com|youtu\\.be)/.+$', url):
|
||||||
if validators.url(url, public=True):
|
if validators.url(url, public=True):
|
||||||
return Response({
|
return Response(
|
||||||
|
{
|
||||||
'recipe_json': get_from_youtube_scraper(url, request),
|
'recipe_json': get_from_youtube_scraper(url, request),
|
||||||
'recipe_images': [],
|
'recipe_images': [],
|
||||||
}, status=status.HTTP_200_OK)
|
}, status=status.HTTP_200_OK)
|
||||||
@ -1392,7 +1413,8 @@ class RecipeUrlImportView(APIView):
|
|||||||
'^(.)*/view/recipe/[0-9]+/[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$',
|
'^(.)*/view/recipe/[0-9]+/[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$',
|
||||||
url):
|
url):
|
||||||
recipe_json = requests.get(
|
recipe_json = requests.get(
|
||||||
url.replace('/view/recipe/', '/api/recipe/').replace(re.split('/view/recipe/[0-9]+', url)[1],
|
url.replace('/view/recipe/', '/api/recipe/').replace(
|
||||||
|
re.split('/view/recipe/[0-9]+', url)[1],
|
||||||
'') + '?share=' +
|
'') + '?share=' +
|
||||||
re.split('/view/recipe/[0-9]+', url)[1].replace('/', '')).json()
|
re.split('/view/recipe/[0-9]+', url)[1].replace('/', '')).json()
|
||||||
recipe_json = clean_dict(recipe_json, 'id')
|
recipe_json = clean_dict(recipe_json, 'id')
|
||||||
@ -1400,13 +1422,17 @@ class RecipeUrlImportView(APIView):
|
|||||||
if serialized_recipe.is_valid():
|
if serialized_recipe.is_valid():
|
||||||
recipe = serialized_recipe.save()
|
recipe = serialized_recipe.save()
|
||||||
if validators.url(recipe_json['image'], public=True):
|
if validators.url(recipe_json['image'], public=True):
|
||||||
recipe.image = File(handle_image(request,
|
recipe.image = File(
|
||||||
File(io.BytesIO(requests.get(recipe_json['image']).content),
|
handle_image(
|
||||||
|
request,
|
||||||
|
File(
|
||||||
|
io.BytesIO(requests.get(recipe_json['image']).content),
|
||||||
name='image'),
|
name='image'),
|
||||||
filetype=pathlib.Path(recipe_json['image']).suffix),
|
filetype=pathlib.Path(recipe_json['image']).suffix),
|
||||||
name=f'{uuid.uuid4()}_{recipe.pk}{pathlib.Path(recipe_json["image"]).suffix}')
|
name=f'{uuid.uuid4()}_{recipe.pk}{pathlib.Path(recipe_json["image"]).suffix}')
|
||||||
recipe.save()
|
recipe.save()
|
||||||
return Response({
|
return Response(
|
||||||
|
{
|
||||||
'link': request.build_absolute_uri(reverse('view_recipe', args={recipe.pk}))
|
'link': request.build_absolute_uri(reverse('view_recipe', args={recipe.pk}))
|
||||||
}, status=status.HTTP_201_CREATED)
|
}, status=status.HTTP_201_CREATED)
|
||||||
else:
|
else:
|
||||||
@ -1415,19 +1441,22 @@ class RecipeUrlImportView(APIView):
|
|||||||
scrape = scrape_me(url_path=url, wild_mode=True)
|
scrape = scrape_me(url_path=url, wild_mode=True)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return Response({
|
return Response(
|
||||||
|
{
|
||||||
'error': True,
|
'error': True,
|
||||||
'msg': _('Invalid Url')
|
'msg': _('Invalid Url')
|
||||||
}, status=status.HTTP_400_BAD_REQUEST)
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
except NoSchemaFoundInWildMode:
|
except NoSchemaFoundInWildMode:
|
||||||
pass
|
pass
|
||||||
except requests.exceptions.ConnectionError:
|
except requests.exceptions.ConnectionError:
|
||||||
return Response({
|
return Response(
|
||||||
|
{
|
||||||
'error': True,
|
'error': True,
|
||||||
'msg': _('Connection Refused.')
|
'msg': _('Connection Refused.')
|
||||||
}, status=status.HTTP_400_BAD_REQUEST)
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
except requests.exceptions.MissingSchema:
|
except requests.exceptions.MissingSchema:
|
||||||
return Response({
|
return Response(
|
||||||
|
{
|
||||||
'error': True,
|
'error': True,
|
||||||
'msg': _('Bad URL Schema.')
|
'msg': _('Bad URL Schema.')
|
||||||
}, status=status.HTTP_400_BAD_REQUEST)
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
@ -1446,13 +1475,15 @@ class RecipeUrlImportView(APIView):
|
|||||||
scrape = text_scraper(text=data, url=found_url)
|
scrape = text_scraper(text=data, url=found_url)
|
||||||
|
|
||||||
if scrape:
|
if scrape:
|
||||||
return Response({
|
return Response(
|
||||||
|
{
|
||||||
'recipe_json': helper.get_from_scraper(scrape, request),
|
'recipe_json': helper.get_from_scraper(scrape, request),
|
||||||
'recipe_images': list(dict.fromkeys(get_images_from_soup(scrape.soup, url))),
|
'recipe_images': list(dict.fromkeys(get_images_from_soup(scrape.soup, url))),
|
||||||
}, status=status.HTTP_200_OK)
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return Response({
|
return Response(
|
||||||
|
{
|
||||||
'error': True,
|
'error': True,
|
||||||
'msg': _('No usable data could be found.')
|
'msg': _('No usable data could be found.')
|
||||||
}, status=status.HTTP_400_BAD_REQUEST)
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
@ -1547,7 +1578,8 @@ def import_files(request):
|
|||||||
|
|
||||||
return Response({'import_id': il.pk}, status=status.HTTP_200_OK)
|
return Response({'import_id': il.pk}, status=status.HTTP_200_OK)
|
||||||
except NotImplementedError:
|
except NotImplementedError:
|
||||||
return Response({'error': True, 'msg': _('Importing is not implemented for this provider')},
|
return Response(
|
||||||
|
{'error': True, 'msg': _('Importing is not implemented for this provider')},
|
||||||
status=status.HTTP_400_BAD_REQUEST)
|
status=status.HTTP_400_BAD_REQUEST)
|
||||||
else:
|
else:
|
||||||
return Response({'error': True, 'msg': form.errors}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({'error': True, 'msg': form.errors}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
@ -1624,7 +1656,8 @@ def get_recipe_file(request, recipe_id):
|
|||||||
@group_required('user')
|
@group_required('user')
|
||||||
def sync_all(request):
|
def sync_all(request):
|
||||||
if request.space.demo or settings.HOSTED:
|
if request.space.demo or settings.HOSTED:
|
||||||
messages.add_message(request, messages.ERROR,
|
messages.add_message(
|
||||||
|
request, messages.ERROR,
|
||||||
_('This feature is not yet available in the hosted version of tandoor!'))
|
_('This feature is not yet available in the hosted version of tandoor!'))
|
||||||
return redirect('index')
|
return redirect('index')
|
||||||
|
|
||||||
@ -1664,7 +1697,8 @@ def share_link(request, pk):
|
|||||||
if request.space.allow_sharing and has_group_permission(request.user, ('user',)):
|
if request.space.allow_sharing and has_group_permission(request.user, ('user',)):
|
||||||
recipe = get_object_or_404(Recipe, pk=pk, space=request.space)
|
recipe = get_object_or_404(Recipe, pk=pk, space=request.space)
|
||||||
link = ShareLink.objects.create(recipe=recipe, created_by=request.user, space=request.space)
|
link = ShareLink.objects.create(recipe=recipe, created_by=request.user, space=request.space)
|
||||||
return JsonResponse({'pk': pk, 'share': link.uuid,
|
return JsonResponse(
|
||||||
|
{'pk': pk, 'share': link.uuid,
|
||||||
'link': request.build_absolute_uri(reverse('view_recipe', args=[pk, link.uuid]))})
|
'link': request.build_absolute_uri(reverse('view_recipe', args=[pk, link.uuid]))})
|
||||||
else:
|
else:
|
||||||
return JsonResponse({'error': 'sharing_disabled'}, status=403)
|
return JsonResponse({'error': 'sharing_disabled'}, status=403)
|
||||||
|
Loading…
Reference in New Issue
Block a user