Merge branch 'develop' into json_import

This commit is contained in:
vabene1111
2021-05-01 22:39:20 +02:00
committed by GitHub
47 changed files with 2451 additions and 861 deletions

View File

@@ -0,0 +1,66 @@
from datetime import datetime, timedelta
from functools import reduce
from django.contrib.postgres.search import TrigramSimilarity
from django.db.models import Q
from cookbook.models import ViewLog
from recipes import settings
def search_recipes(request, queryset, params):
search_string = params.get('query', '')
search_keywords = params.getlist('keywords', [])
search_foods = params.getlist('foods', [])
search_books = params.getlist('books', [])
search_keywords_or = params.get('keywords_or', True)
search_foods_or = params.get('foods_or', True)
search_books_or = params.get('books_or', True)
search_internal = params.get('internal', None)
search_random = params.get('random', False)
search_last_viewed = int(params.get('last_viewed', 0))
if search_last_viewed > 0:
last_viewed_recipes = ViewLog.objects.filter(created_by=request.user, space=request.space, created_at__gte=datetime.now() - timedelta(days=14)).values_list('recipe__pk', flat=True).distinct()
return queryset.filter(pk__in=list(set(last_viewed_recipes))[-search_last_viewed:])
if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2',
'django.db.backends.postgresql']:
queryset = queryset.annotate(similarity=TrigramSimilarity('name', search_string), ).filter(
Q(similarity__gt=0.1) | Q(name__unaccent__icontains=search_string)).order_by('-similarity')
else:
queryset = queryset.filter(name__icontains=search_string)
if len(search_keywords) > 0:
if search_keywords_or == 'true':
queryset = queryset.filter(keywords__id__in=search_keywords)
else:
for k in search_keywords:
queryset = queryset.filter(keywords__id=k)
if len(search_foods) > 0:
if search_foods_or == 'true':
queryset = queryset.filter(steps__ingredients__food__id__in=search_foods)
else:
for k in search_foods:
queryset = queryset.filter(steps__ingredients__food__id=k)
if len(search_books) > 0:
if search_books_or == 'true':
queryset = queryset.filter(recipebookentry__book__id__in=search_books)
else:
for k in search_books:
queryset = queryset.filter(recipebookentry__book__id=k)
queryset = queryset.distinct()
if search_internal == 'true':
queryset = queryset.filter(internal=True)
if search_random == 'true':
queryset = queryset.order_by("?")
return queryset

View File

@@ -20,8 +20,10 @@ class Paprika(Integration):
recipe_json = json.loads(recipe_zip.read().decode("utf-8"))
recipe = Recipe.objects.create(
name=recipe_json['name'].strip(), description=recipe_json['description'].strip(),
created_by=self.request.user, internal=True, space=self.request.space)
name=recipe_json['name'].strip(), created_by=self.request.user, internal=True, space=self.request.space)
if 'description' in recipe_json:
recipe.description = recipe_json['description'].strip()
try:
if re.match(r'([0-9])+\s(.)*', recipe_json['servings'] ):
@@ -40,14 +42,17 @@ class Paprika(Integration):
recipe.save()
instructions = recipe_json['directions']
if len(recipe_json['notes'].strip()) > 0:
if recipe_json['notes'] and len(recipe_json['notes'].strip()) > 0:
instructions += '\n\n### ' + _('Notes') + ' \n' + recipe_json['notes']
if len(recipe_json['nutritional_info'].strip()) > 0:
if recipe_json['nutritional_info'] and len(recipe_json['nutritional_info'].strip()) > 0:
instructions += '\n\n### ' + _('Nutritional Information') + ' \n' + recipe_json['nutritional_info']
if len(recipe_json['source'].strip()) > 0 or len(recipe_json['source_url'].strip()) > 0:
instructions += '\n\n### ' + _('Source') + ' \n' + recipe_json['source'].strip() + ' \n' + recipe_json['source_url'].strip()
try:
if len(recipe_json['source'].strip()) > 0 or len(recipe_json['source_url'].strip()) > 0:
instructions += '\n\n### ' + _('Source') + ' \n' + recipe_json['source'].strip() + ' \n' + recipe_json['source_url'].strip()
except AttributeError:
pass
step = Step.objects.create(
instruction=instructions
@@ -58,16 +63,21 @@ class Paprika(Integration):
keyword, created = Keyword.objects.get_or_create(name=c.strip(), space=self.request.space)
recipe.keywords.add(keyword)
for ingredient in recipe_json['ingredients'].split('\n'):
if len(ingredient.strip()) > 0:
amount, unit, ingredient, note = parse(ingredient)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
))
try:
for ingredient in recipe_json['ingredients'].split('\n'):
if len(ingredient.strip()) > 0:
amount, unit, ingredient, note = parse(ingredient)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
))
except AttributeError:
pass
recipe.steps.add(step)
self.import_recipe_image(recipe, BytesIO(base64.b64decode(recipe_json['photo_data'])))
if recipe_json.get("photo_data", None):
self.import_recipe_image(recipe, BytesIO(base64.b64decode(recipe_json['photo_data'])))
return recipe

View File

@@ -15,8 +15,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-11 15:09+0200\n"
"PO-Revision-Date: 2021-04-11 15:23+0000\n"
"Last-Translator: vabene1111 <vabene1234@googlemail.com>\n"
"PO-Revision-Date: 2021-05-01 13:01+0000\n"
"Last-Translator: Marcel Paluch <marcelpaluch@icloud.com>\n"
"Language-Team: German <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/de/>\n"
"Language: de\n"
@@ -43,7 +43,9 @@ msgstr ""
#: .\cookbook\forms.py:46
msgid "Default Unit to be used when inserting a new ingredient into a recipe."
msgstr "Standardeinheit für neue Zutaten."
msgstr ""
"Standardeinheit, die beim Einfügen einer neuen Zutat in ein Rezept zu "
"verwenden ist."
#: .\cookbook\forms.py:47
msgid ""
@@ -71,7 +73,9 @@ msgstr "Anzahl an Dezimalstellen, auf die gerundet werden soll."
#: .\cookbook\forms.py:51
msgid "If you want to be able to create and see comments underneath recipes."
msgstr "Ob Kommentare unter Rezepten erstellt und angesehen werden können."
msgstr ""
"Wenn du in der Lage sein willst, Kommentare unter Rezepten zu erstellen und "
"zu sehen."
#: .\cookbook\forms.py:53
msgid ""
@@ -94,7 +98,7 @@ msgid ""
"Both fields are optional. If none are given the username will be displayed "
"instead"
msgstr ""
"Beide Felder sind optional, wenn keins von beiden gegeben ist, wird der "
"Beide Felder sind optional. Wenn keins von beiden gegeben ist, wird der "
"Nutzername angezeigt"
#: .\cookbook\forms.py:93 .\cookbook\forms.py:315
@@ -123,7 +127,7 @@ msgstr "Pfad"
#: .\cookbook\forms.py:98
msgid "Storage UID"
msgstr "Speicher-ID"
msgstr "Speicher-UID"
#: .\cookbook\forms.py:121
msgid "Default"
@@ -135,7 +139,7 @@ msgid ""
"ignored. Check this box to import everything."
msgstr ""
"Um Duplikate zu vermeiden werden Rezepte mit dem gleichen Namen ignoriert. "
"Checken sie diese Box um alle Rezepte zu importieren."
"Aktivieren Sie dieses Kontrollkästchen, um alles zu importieren."
#: .\cookbook\forms.py:149
msgid "New Unit"
@@ -204,8 +208,8 @@ msgstr "Mindestens ein Rezept oder ein Titel müssen angegeben werden."
#: .\cookbook\forms.py:367
msgid "You can list default users to share recipes with in the settings."
msgstr ""
"Benutzer, mit denen neue Rezepte standardmäßig geteilt werden sollen, können "
"in den Einstellungen angegeben werden."
"Sie können in den Einstellungen Standardbenutzer auflisten, für die Sie "
"Rezepte freigeben möchten."
#: .\cookbook\forms.py:368
#: .\cookbook\templates\forms\edit_internal_recipe.html:377
@@ -213,8 +217,8 @@ msgid ""
"You can use markdown to format this field. See the <a href=\"/docs/markdown/"
"\">docs here</a>"
msgstr ""
"Markdown kann genutzt werden, um dieses Feld zu formatieren. Siehe <a href="
"\"/docs/markdown/\">hier</a> für weitere Information."
"Markdown kann genutzt werden, um dieses Feld zu formatieren. Siehe <a href=\""
"/docs/markdown/\">hier</a> für weitere Information"
#: .\cookbook\forms.py:393
msgid "A username is not required, if left blank the new user can choose one."
@@ -309,8 +313,9 @@ msgstr "Quelle"
#: .\cookbook\templates\forms\edit_internal_recipe.html:75
#: .\cookbook\templates\include\log_cooking.html:16
#: .\cookbook\templates\url_import.html:84
#, fuzzy
msgid "Servings"
msgstr "Portion(en)"
msgstr "Portionen"
#: .\cookbook\integration\safron.py:25
msgid "Waiting time"

View File

@@ -13,7 +13,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-11 15:09+0200\n"
"PO-Revision-Date: 2021-04-12 20:22+0000\n"
"PO-Revision-Date: 2021-04-22 18:29+0000\n"
"Last-Translator: Jesse <jesse.kamps@pm.me>\n"
"Language-Team: Dutch <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/nl/>\n"
@@ -96,7 +96,7 @@ msgid ""
"instead"
msgstr ""
"Beide velden zijn optioneel. Indien niks is opgegeven wordt de "
"gebruikersnaam weergegeven."
"gebruikersnaam weergegeven"
#: .\cookbook\forms.py:93 .\cookbook\forms.py:315
#: .\cookbook\templates\forms\edit_internal_recipe.html:45

View File

@@ -115,8 +115,9 @@ class UserPreference(models.Model, PermissionModelMixin):
# Search Style
SMALL = 'SMALL'
LARGE = 'LARGE'
NEW = 'NEW'
SEARCH_STYLE = ((SMALL, _('Small')), (LARGE, _('Large')),)
SEARCH_STYLE = ((SMALL, _('Small')), (LARGE, _('Large')), (NEW, _('New')))
user = AutoOneToOneField(User, on_delete=models.CASCADE, primary_key=True)
theme = models.CharField(choices=THEMES, max_length=128, default=FLATLY)
@@ -420,6 +421,9 @@ class Comment(models.Model, PermissionModelMixin):
def get_space_key():
return 'recipe', 'space'
def get_space(self):
return self.recipe.space
def __str__(self):
return self.text

View File

@@ -269,6 +269,12 @@ class NutritionInformationSerializer(serializers.ModelSerializer):
class RecipeOverviewSerializer(WritableNestedModelSerializer):
keywords = KeywordLabelSerializer(many=True)
def create(self, validated_data):
pass
def update(self, instance, validated_data):
return instance
class Meta:
model = Recipe
fields = (
@@ -342,7 +348,8 @@ class RecipeBookEntrySerializer(serializers.ModelSerializer):
fields = ('id', 'book', 'recipe',)
class MealPlanSerializer(SpacedModelSerializer):
class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
recipe = RecipeOverviewSerializer(required=False)
recipe_name = serializers.ReadOnlyField(source='recipe.name')
meal_type_name = serializers.ReadOnlyField(source='meal_type.name')
note_markdown = serializers.SerializerMethodField('get_note_markdown')

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -27,6 +27,5 @@
window.IMPORT_ID = {{pk}};
</script>
{% render_bundle 'chunk-vendors' %}
{% render_bundle 'import_response_view' %}
{% endblock %}

View File

@@ -509,28 +509,28 @@
this.getRecipes();
},
getRecipes: function () {
let url = "{% url 'api:recipe-list' %}?limit=5"
let url = "{% url 'api:recipe-list' %}?page_size=5"
if (this.recipe_query !== '') {
url += '&query=' + this.recipe_query;
} else {
url += '&random=True'
url += '&random=true'
}
this.$http.get(url).then((response) => {
this.recipes = response.data;
this.recipes = response.data.results;
}).catch((err) => {
console.log("getRecipes error: ", err);
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
getMdNote: function () {
let url = "{% url 'api:recipe-list' %}?limit=5"
let url = "{% url 'api:recipe-list' %}?page_size=5"
if (this.recipe_query !== '') {
url += '&query=' + this.recipe_query;
}
this.$http.get(url).then((response) => {
this.recipes = response.data;
this.recipes = response.data.results;
}).catch((err) => {
console.log("getRecipes error: ", err);
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
@@ -627,13 +627,14 @@
cloneRecipe: function (recipe) {
let r = {
id: Math.round(Math.random() * 1000) + 10000,
recipe: recipe.id,
recipe: recipe,
recipe_name: recipe.name,
servings: (this.new_note_servings > 1) ? this.new_note_servings : recipe.servings,
title: this.new_note_title,
note: this.new_note_text,
is_new: true
}
console.log(recipe)
this.new_note_title = ''
this.new_note_text = ''
@@ -669,7 +670,7 @@
}
},
planDetailRecipeUrl: function () {
return "{% url 'view_recipe' 12345 %}".replace(/12345/, this.plan_detail.recipe);
return "{% url 'view_recipe' 12345 %}".replace(/12345/, this.plan_detail.recipe.id);
},
planDetailEditUrl: function () {
return "{% url 'edit_meal_plan' 12345 %}".replace(/12345/, this.plan_detail.id);

View File

@@ -38,8 +38,6 @@
<script src="{% static 'django_js_reverse/reverse.js' %}"></script>
{% endif %}
{% render_bundle 'chunk-vendors' %}
<!--
yes this is a stupid solution! i need to figure out a better way to do this but the version hashes
of djangos static files prevent my from simply using preCacheAndRoute

View File

@@ -70,6 +70,5 @@
}
</script>
{% render_bundle 'chunk-vendors' %}
{% render_bundle 'recipe_view' %}
{% endblock %}

View File

@@ -8,7 +8,7 @@
{% block content_fluid %}
<div id="app">
<div id="app" >
<recipe-search-view></recipe-search-view>
</div>
@@ -24,9 +24,8 @@
{% endif %}
<script type="application/javascript">
window.IMAGE_PLACEHOLDER = "{% static 'assets/recipe_no_image.svg' %}"
</script>
{% render_bundle 'chunk-vendors' %}
{% render_bundle 'recipe_search_view' %}
{% endblock %}

View File

@@ -825,7 +825,7 @@
}
},
getRecipes: function () {
let url = "{% url 'api:recipe-list' %}?limit=5&internal=true"
let url = "{% url 'api:recipe-list' %}?page_size=5&internal=true"
if (this.recipe_query !== '') {
url += '&query=' + this.recipe_query;
} else {
@@ -834,7 +834,7 @@
}
this.$http.get(url).then((response) => {
this.recipes = response.data;
this.recipes = response.data.results;
}).catch((err) => {
console.log("getRecipes error: ", err);
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')

File diff suppressed because one or more lines are too long

View File

@@ -19,12 +19,14 @@ def meal_type(space_1, u1_s1):
@pytest.fixture()
def obj_1(space_1, recipe_1_s1, meal_type, u1_s1):
return MealPlan.objects.create(recipe=recipe_1_s1, space=space_1, meal_type=meal_type, date=datetime.now(), created_by=auth.get_user(u1_s1))
return MealPlan.objects.create(recipe=recipe_1_s1, space=space_1, meal_type=meal_type, date=datetime.now(),
created_by=auth.get_user(u1_s1))
@pytest.fixture
def obj_2(space_1, recipe_1_s1, meal_type, u1_s1):
return MealPlan.objects.create(recipe=recipe_1_s1, space=space_1, meal_type=meal_type, date=datetime.now(), created_by=auth.get_user(u1_s1))
return MealPlan.objects.create(recipe=recipe_1_s1, space=space_1, meal_type=meal_type, date=datetime.now(),
created_by=auth.get_user(u1_s1))
@pytest.mark.parametrize("arg", [
@@ -55,13 +57,16 @@ def test_list_filter(obj_1, u1_s1):
response = json.loads(r.content)
assert len(response) == 1
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?from_date={(datetime.now() + timedelta(days=2)).strftime("%Y-%m-%d")}').content)
response = json.loads(
u1_s1.get(f'{reverse(LIST_URL)}?from_date={(datetime.now() + timedelta(days=2)).strftime("%Y-%m-%d")}').content)
assert len(response) == 0
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?to_date={(datetime.now() - timedelta(days=2)).strftime("%Y-%m-%d")}').content)
response = json.loads(
u1_s1.get(f'{reverse(LIST_URL)}?to_date={(datetime.now() - timedelta(days=2)).strftime("%Y-%m-%d")}').content)
assert len(response) == 0
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?from_date={(datetime.now() - timedelta(days=2)).strftime("%Y-%m-%d")}&to_date={(datetime.now() + timedelta(days=2)).strftime("%Y-%m-%d")}').content)
response = json.loads(u1_s1.get(
f'{reverse(LIST_URL)}?from_date={(datetime.now() - timedelta(days=2)).strftime("%Y-%m-%d")}&to_date={(datetime.now() + timedelta(days=2)).strftime("%Y-%m-%d")}').content)
assert len(response) == 1
@@ -100,10 +105,12 @@ def test_add(arg, request, u1_s2, recipe_1_s1, meal_type):
c = request.getfixturevalue(arg[0])
r = c.post(
reverse(LIST_URL),
{'recipe': recipe_1_s1.id, 'meal_type': meal_type.id, 'date': (datetime.now()).strftime("%Y-%m-%d"), 'servings': 1, 'title': 'test'},
{'recipe': {'id': recipe_1_s1.id, 'name': recipe_1_s1.name, 'keywords': []}, 'meal_type': meal_type.id,
'date': (datetime.now()).strftime("%Y-%m-%d"), 'servings': 1, 'title': 'test'},
content_type='application/json'
)
response = json.loads(r.content)
print(response)
assert r.status_code == arg[1]
if r.status_code == 201:
assert response['title'] == 'test'

View File

@@ -22,15 +22,15 @@ def test_list_permission(arg, request):
def test_list_space(recipe_1_s1, u1_s1, u1_s2, space_2):
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 1
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)['results']) == 1
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)['results']) == 0
with scopes_disabled():
recipe_1_s1.space = space_2
recipe_1_s1.save()
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 0
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 1
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)['results']) == 0
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)['results']) == 1
@pytest.mark.parametrize("arg", [

View File

@@ -16,19 +16,33 @@ from django.http import FileResponse, HttpResponse, JsonResponse
from django.shortcuts import redirect, get_object_or_404
from django.utils.translation import gettext as _
from icalendar import Calendar, Event
from rest_framework import decorators, viewsets
from rest_framework.exceptions import APIException, PermissionDenied
from recipe_scrapers import scrape_me, WebsiteNotImplementedError, NoSchemaFoundInWildMode
from rest_framework import decorators, viewsets
from rest_framework.exceptions import APIException, PermissionDenied
from rest_framework.pagination import PageNumberPagination
from rest_framework.parsers import MultiPartParser
from rest_framework.response import Response
from rest_framework.schemas.openapi import AutoSchema
from rest_framework.schemas.utils import is_list_view
from rest_framework.viewsets import ViewSetMixin
from cookbook.helper.ingredient_parser import parse
from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest,
CustomIsOwner, CustomIsShare,
CustomIsShared, CustomIsUser,
group_required, share_link_valid)
from cookbook.helper.recipe_html_import import get_recipe_from_source
from cookbook.helper.recipe_url_import import get_from_scraper
group_required)
from cookbook.helper.recipe_search import search_recipes
from cookbook.helper.recipe_url_import import get_from_html, get_from_scraper, find_recipe_json
from cookbook.models import (CookLog, Food, Ingredient, Keyword, MealPlan,
MealType, Recipe, RecipeBook, ShoppingList,
ShoppingListEntry, ShoppingListRecipe, Step,
@@ -48,9 +62,9 @@ from cookbook.serializer import (FoodSerializer, IngredientSerializer,
StorageSerializer, SyncLogSerializer,
SyncSerializer, UnitSerializer,
UserNameSerializer, UserPreferenceSerializer,
ViewLogSerializer, CookLogSerializer, RecipeBookEntrySerializer, RecipeOverviewSerializer, SupermarketSerializer, ImportLogSerializer)
ViewLogSerializer, CookLogSerializer, RecipeBookEntrySerializer,
RecipeOverviewSerializer, SupermarketSerializer, ImportLogSerializer)
from recipes.settings import DEMO
from recipe_scrapers import scrape_me, WebsiteNotImplementedError, NoSchemaFoundInWildMode
class StandardFilterMixin(ViewSetMixin):
@@ -247,7 +261,8 @@ class MealTypeViewSet(viewsets.ModelViewSet):
permission_classes = [CustomIsOwner]
def get_queryset(self):
queryset = self.queryset.order_by('order', 'id').filter(created_by=self.request.user).filter(space=self.request.space).all()
queryset = self.queryset.order_by('order', 'id').filter(created_by=self.request.user).filter(
space=self.request.space).all()
return queryset
@@ -269,33 +284,88 @@ class StepViewSet(viewsets.ModelViewSet):
return self.queryset.filter(recipe__space=self.request.space)
class RecipeViewSet(viewsets.ModelViewSet, StandardFilterMixin):
"""
list:
optional parameters
class RecipePagination(PageNumberPagination):
page_size = 25
page_size_query_param = 'page_size'
max_page_size = 100
- **query**: search recipes for a string contained
in the recipe name (case in-sensitive)
- **limit**: limits the amount of returned results
"""
# TODO move to separate class to cleanup
class RecipeSchema(AutoSchema):
def get_path_parameters(self, path, method):
if not is_list_view(path, method, self.view):
return []
parameters = super().get_path_parameters(path, method)
parameters.append({
"name": 'query', "in": "query", "required": False,
"description": 'Query string matched (fuzzy) against recipe name. In the future also fulltext search.',
'schema': {'type': 'string', },
})
parameters.append({
"name": 'keywords', "in": "query", "required": False,
"description": 'Id of keyword a recipe should have. For multiple repeat parameter.',
'schema': {'type': 'string', },
})
parameters.append({
"name": 'foods', "in": "query", "required": False,
"description": 'Id of food a recipe should have. For multiple repeat parameter.',
'schema': {'type': 'string', },
})
parameters.append({
"name": 'books', "in": "query", "required": False,
"description": 'Id of book a recipe should have. For multiple repeat parameter.',
'schema': {'type': 'string', },
})
parameters.append({
"name": 'keywords_or', "in": "query", "required": False,
"description": 'If recipe should have all (AND) or any (OR) of the provided keywords.',
'schema': {'type': 'string', },
})
parameters.append({
"name": 'foods_or', "in": "query", "required": False,
"description": 'If recipe should have all (AND) or any (OR) any of the provided foods.',
'schema': {'type': 'string', },
})
parameters.append({
"name": 'books_or', "in": "query", "required": False,
"description": 'If recipe should be in all (AND) or any (OR) any of the provided books.',
'schema': {'type': 'string', },
})
parameters.append({
"name": 'internal', "in": "query", "required": False,
"description": 'true or false. If only internal recipes should be returned or not.',
'schema': {'type': 'string', },
})
parameters.append({
"name": 'random', "in": "query", "required": False,
"description": 'true or false. returns the results in randomized order.',
'schema': {'type': 'string', },
})
return parameters
class RecipeViewSet(viewsets.ModelViewSet):
queryset = Recipe.objects
serializer_class = RecipeSerializer
# TODO split read and write permission for meal plan guest
permission_classes = [CustomIsShare | CustomIsGuest]
pagination_class = RecipePagination
schema = RecipeSchema()
def get_queryset(self):
share = self.request.query_params.get('share', None)
if not (share and self.detail):
self.queryset = self.queryset.filter(space=self.request.space)
internal = self.request.query_params.get('internal', None)
if internal:
self.queryset = self.queryset.filter(internal=True)
self.queryset = search_recipes(self.request, self.queryset, self.request.GET)
return super().get_queryset()
# TODO write extensive tests for permissions
def get_serializer_class(self):
if self.action == 'list':
return RecipeOverviewSerializer
@@ -344,7 +414,9 @@ class ShoppingListRecipeViewSet(viewsets.ModelViewSet):
permission_classes = [CustomIsOwner | CustomIsShared]
def get_queryset(self):
return self.queryset.filter(Q(shoppinglist__created_by=self.request.user) | Q(shoppinglist__shared=self.request.user)).filter(shoppinglist__space=self.request.space).all()
return self.queryset.filter(
Q(shoppinglist__created_by=self.request.user) | Q(shoppinglist__shared=self.request.user)).filter(
shoppinglist__space=self.request.space).all()
class ShoppingListEntryViewSet(viewsets.ModelViewSet):
@@ -353,7 +425,9 @@ class ShoppingListEntryViewSet(viewsets.ModelViewSet):
permission_classes = [CustomIsOwner | CustomIsShared]
def get_queryset(self):
return self.queryset.filter(Q(shoppinglist__created_by=self.request.user) | Q(shoppinglist__shared=self.request.user)).filter(shoppinglist__space=self.request.space).all()
return self.queryset.filter(
Q(shoppinglist__created_by=self.request.user) | Q(shoppinglist__shared=self.request.user)).filter(
shoppinglist__space=self.request.space).all()
class ShoppingListViewSet(viewsets.ModelViewSet):
@@ -362,7 +436,8 @@ class ShoppingListViewSet(viewsets.ModelViewSet):
permission_classes = [CustomIsOwner | CustomIsShared]
def get_queryset(self):
return self.queryset.filter(Q(created_by=self.request.user) | Q(shared=self.request.user)).filter(space=self.request.space).distinct()
return self.queryset.filter(Q(created_by=self.request.user) | Q(shared=self.request.user)).filter(
space=self.request.space).distinct()
def get_serializer_class(self):
try:
@@ -603,6 +678,7 @@ def recipe_from_source(request):
'images': images,
})
else:
return JsonResponse(
{

View File

@@ -14,6 +14,7 @@ from django.utils.translation import gettext as _
from django.utils.translation import ngettext
from django_tables2 import RequestConfig
from PIL import Image, UnidentifiedImageError
from requests.exceptions import MissingSchema
from cookbook.forms import BatchEditForm, SyncForm
from cookbook.helper.permission_helper import group_required, has_group_permission
@@ -168,7 +169,7 @@ def import_url(request):
step.ingredients.add(ingredient)
print(ingredient)
if 'image' in data and data['image'] != '':
if 'image' in data and data['image'] != '' and data['image'] is not None:
try:
response = requests.get(data['image'])
img = Image.open(BytesIO(response.content))
@@ -187,6 +188,8 @@ def import_url(request):
recipe.save()
except UnidentifiedImageError:
pass
except MissingSchema:
pass
return HttpResponse(reverse('view_recipe', args=[recipe.pk]))

View File

@@ -55,6 +55,9 @@ def index(request):
def search(request):
if has_group_permission(request.user, ('guest',)):
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('name'),
space=request.space)