mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-01 12:18:45 -05:00
Merge branch 'develop' into json_import
This commit is contained in:
66
cookbook/helper/recipe_search.py
Normal file
66
cookbook/helper/recipe_search.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -27,6 +27,5 @@
|
||||
window.IMPORT_ID = {{pk}};
|
||||
</script>
|
||||
|
||||
{% render_bundle 'chunk-vendors' %}
|
||||
{% render_bundle 'import_response_view' %}
|
||||
{% endblock %}
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -70,6 +70,5 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{% render_bundle 'chunk-vendors' %}
|
||||
{% render_bundle 'recipe_view' %}
|
||||
{% endblock %}
|
||||
@@ -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 %}
|
||||
@@ -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
@@ -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'
|
||||
|
||||
@@ -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", [
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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]))
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user