Merge branch 'develop' into json_import
This commit is contained in:
2
.idea/recipes.iml
generated
2
.idea/recipes.iml
generated
@ -6,7 +6,7 @@
|
||||
<option name="rootFolder" value="$MODULE_DIR$" />
|
||||
<option name="settingsModule" value="recipes/settings.py" />
|
||||
<option name="manageScript" value="$MODULE_DIR$/manage.py" />
|
||||
<option name="environment" value="<map> <entry> <string>POSTGRES_USER</string> <string>postgres</string> </entry> <entry> <string>POSTGRES_HOST</string> <string>localhost</string> </entry> <entry> <string>DB_ENGINE</string> <string>django.db.backends.postgresql_psycopg2</string> </entry> <entry> <string>POSTGRES_PORT</string> <string>5432</string> </entry> <entry> <string>POSTGRES_PASSWORD</string> <string>Computer1234</string> </entry> <entry> <string>POSTGRES_DB</string> <string>recipes_db</string> </entry> </map>" />
|
||||
<option name="environment" value="<map/>" />
|
||||
<option name="doNotUseTestRunner" value="false" />
|
||||
<option name="trackFilePattern" value="migrations" />
|
||||
</configuration>
|
||||
|
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']
|
||||
|
||||
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,6 +63,7 @@ class Paprika(Integration):
|
||||
keyword, created = Keyword.objects.get_or_create(name=c.strip(), space=self.request.space)
|
||||
recipe.keywords.add(keyword)
|
||||
|
||||
try:
|
||||
for ingredient in recipe_json['ingredients'].split('\n'):
|
||||
if len(ingredient.strip()) > 0:
|
||||
amount, unit, ingredient, note = parse(ingredient)
|
||||
@ -66,8 +72,12 @@ class Paprika(Integration):
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=amount, note=note
|
||||
))
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
recipe.steps.add(step)
|
||||
|
||||
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)
|
||||
|
16
node_modules/.yarn-integrity
generated
vendored
Normal file
16
node_modules/.yarn-integrity
generated
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"systemParams": "win32-x64-83",
|
||||
"modulesFolders": [
|
||||
"node_modules"
|
||||
],
|
||||
"flags": [],
|
||||
"linkedModules": [],
|
||||
"topLevelPatterns": [
|
||||
"vue-cookies@^1.7.4"
|
||||
],
|
||||
"lockfileEntries": {
|
||||
"vue-cookies@^1.7.4": "https://registry.yarnpkg.com/vue-cookies/-/vue-cookies-1.7.4.tgz#d241d0a0431da0795837651d10b4d73e7c8d3e8d"
|
||||
},
|
||||
"files": [],
|
||||
"artifacts": {}
|
||||
}
|
21
node_modules/vue-cookies/LICENSE
generated
vendored
Normal file
21
node_modules/vue-cookies/LICENSE
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
223
node_modules/vue-cookies/README.md
generated
vendored
Normal file
223
node_modules/vue-cookies/README.md
generated
vendored
Normal file
@ -0,0 +1,223 @@
|
||||
# vue-cookies
|
||||
|
||||
A simple Vue.js plugin for handling browser cookies
|
||||
|
||||
## Installation
|
||||
|
||||
### Browser
|
||||
```
|
||||
<script src="https://unpkg.com/vue/dist/vue.js"></script>
|
||||
<script src="https://unpkg.com/vue-cookies@1.7.4/vue-cookies.js"></script>
|
||||
```
|
||||
### Package Managers
|
||||
```
|
||||
npm install vue-cookies --save
|
||||
|
||||
// require
|
||||
var Vue = require('vue')
|
||||
Vue.use(require('vue-cookies'))
|
||||
|
||||
// es2015 module
|
||||
import Vue from 'vue'
|
||||
import VueCookies from 'vue-cookies'
|
||||
Vue.use(VueCookies)
|
||||
|
||||
// set default config
|
||||
Vue.$cookies.config('7d')
|
||||
|
||||
// set global cookie
|
||||
Vue.$cookies.set('theme','default');
|
||||
Vue.$cookies.set('hover-time','1s');
|
||||
```
|
||||
|
||||
## Api
|
||||
|
||||
syntax format: **[this | Vue].$cookies.[method]**
|
||||
|
||||
* Set global config
|
||||
```
|
||||
$cookies.config(expireTimes[,path[, domain[, secure[, sameSite]]]) // default: expireTimes = 1d, path = '/', domain = '', secure = '', sameSite = 'Lax'
|
||||
```
|
||||
|
||||
* Set a cookie
|
||||
```
|
||||
$cookies.set(keyName, value[, expireTimes[, path[, domain[, secure[, sameSite]]]]]) //return this
|
||||
```
|
||||
* Get a cookie
|
||||
```
|
||||
$cookies.get(keyName) // return value
|
||||
```
|
||||
* Remove a cookie
|
||||
```
|
||||
$cookies.remove(keyName [, path [, domain]]) // return this
|
||||
```
|
||||
* Exist a `cookie name`
|
||||
```
|
||||
$cookies.isKey(keyName) // return false or true
|
||||
```
|
||||
* Get All `cookie name`
|
||||
```
|
||||
$cookies.keys() // return a array
|
||||
```
|
||||
|
||||
## Example Usage
|
||||
|
||||
#### set global config
|
||||
```
|
||||
// 30 day after, expire
|
||||
Vue.$cookies.config('30d')
|
||||
|
||||
// set secure, only https works
|
||||
Vue.$cookies.config('7d','','',true)
|
||||
|
||||
// 2019-03-13 expire
|
||||
this.$cookies.config(new Date(2019,03,13).toUTCString())
|
||||
|
||||
// 30 day after, expire, '' current path , browser default
|
||||
this.$cookies.config(60 * 60 * 24 * 30,'');
|
||||
|
||||
```
|
||||
|
||||
#### support json object
|
||||
```
|
||||
var user = { id:1, name:'Journal',session:'25j_7Sl6xDq2Kc3ym0fmrSSk2xV2XkUkX' };
|
||||
this.$cookies.set('user',user);
|
||||
// print user name
|
||||
console.log(this.$cookies.get('user').name)
|
||||
```
|
||||
|
||||
#### set expire times
|
||||
**Suppose the current time is : Sat, 11 Mar 2017 12:25:57 GMT**
|
||||
|
||||
**Following equivalence: 1 day after, expire**
|
||||
|
||||
**Support chaining sets together**
|
||||
``` javascript
|
||||
// default expire time: 1 day
|
||||
this.$cookies.set("user_session","25j_7Sl6xDq2Kc3ym0fmrSSk2xV2XkUkX")
|
||||
// number + d , ignore case
|
||||
.set("user_session","25j_7Sl6xDq2Kc3ym0fmrSSk2xV2XkUkX","1d")
|
||||
.set("user_session","25j_7Sl6xDq2Kc3ym0fmrSSk2xV2XkUkX","1D")
|
||||
// Base of second
|
||||
.set("user_session","25j_7Sl6xDq2Kc3ym0fmrSSk2xV2XkUkX",60 * 60 * 24)
|
||||
// input a Date, + 1day
|
||||
.set("user_session","25j_7Sl6xDq2Kc3ym0fmrSSk2xV2XkUkX", new Date(2017, 03, 12))
|
||||
// input a date string, + 1day
|
||||
.set("user_session","25j_7Sl6xDq2Kc3ym0fmrSSk2xV2XkUkX", "Sat, 13 Mar 2017 12:25:57 GMT")
|
||||
```
|
||||
#### set expire times, input number type
|
||||
|
||||
```
|
||||
this.$cookies.set("default_unit_second","input_value",1); // 1 second after, expire
|
||||
this.$cookies.set("default_unit_second","input_value",60 + 30); // 1 minute 30 second after, expire
|
||||
this.$cookies.set("default_unit_second","input_value",60 * 60 * 12); // 12 hour after, expire
|
||||
this.$cookies.set("default_unit_second","input_value",60 * 60 * 24 * 30); // 1 month after, expire
|
||||
```
|
||||
|
||||
#### set expire times - end of browser session
|
||||
|
||||
```
|
||||
this.$cookies.set("default_unit_second","input_value",0); // end of session - use 0 or "0"!
|
||||
```
|
||||
|
||||
|
||||
#### set expire times , input string type
|
||||
|
||||
| Unit | full name |
|
||||
| ----------- | ----------- |
|
||||
| y | year |
|
||||
| m | month |
|
||||
| d | day |
|
||||
| h | hour |
|
||||
| min | minute |
|
||||
| s | second |
|
||||
|
||||
**Unit Names Ignore Case**
|
||||
|
||||
**not support the combination**
|
||||
|
||||
**not support the double value**
|
||||
|
||||
```javascript
|
||||
this.$cookies.set("token","GH1.1.1689020474.1484362313","60s"); // 60 second after, expire
|
||||
this.$cookies.set("token","GH1.1.1689020474.1484362313","30MIN"); // 30 minute after, expire, ignore case
|
||||
this.$cookies.set("token","GH1.1.1689020474.1484362313","24d"); // 24 day after, expire
|
||||
this.$cookies.set("token","GH1.1.1689020474.1484362313","4m"); // 4 month after, expire
|
||||
this.$cookies.set("token","GH1.1.1689020474.1484362313","16h"); // 16 hour after, expire
|
||||
this.$cookies.set("token","GH1.1.1689020474.1484362313","3y"); // 3 year after, expire
|
||||
|
||||
// input date string
|
||||
this.$cookies.set('token',"GH1.1.1689020474.1484362313", new Date(2017,3,13).toUTCString());
|
||||
this.$cookies.set("token","GH1.1.1689020474.1484362313", "Sat, 13 Mar 2017 12:25:57 GMT ");
|
||||
```
|
||||
|
||||
#### set expire support date
|
||||
```
|
||||
var date = new Date;
|
||||
date.setDate(date.getDate() + 1);
|
||||
this.$cookies.set("token","GH1.1.1689020474.1484362313", date);
|
||||
```
|
||||
|
||||
#### set never expire
|
||||
```
|
||||
this.$cookies.set("token","GH1.1.1689020474.1484362313", Infinity); // never expire
|
||||
// never expire , only -1,Other negative Numbers are invalid
|
||||
this.$cookies.set("token","GH1.1.1689020474.1484362313", -1);
|
||||
```
|
||||
|
||||
#### remove cookie
|
||||
```
|
||||
this.$cookies.set("token",value); // domain.com and *.doamin.com are readable
|
||||
this.$cookies.remove("token"); // remove token of domain.com and *.doamin.com
|
||||
|
||||
this.$cookies.set("token", value, null, null, "domain.com"); // only domain.com are readable
|
||||
this.$cookies.remove("token", null, "domain.com"); // remove token of domain.com
|
||||
```
|
||||
|
||||
#### set other arguments
|
||||
```
|
||||
// set path
|
||||
this.$cookies.set("use_path_argument","value","1d","/app");
|
||||
|
||||
// set domain
|
||||
this.$cookies.set("use_path_argument","value",null, null, "domain.com"); // default 1 day after,expire
|
||||
|
||||
// set secure
|
||||
this.$cookies.set("use_path_argument","value",null, null, null,true);
|
||||
|
||||
// set sameSite - should be one of `None`, `Strict` or `Lax`. Read more https://web.dev/samesite-cookies-explained/
|
||||
this.$cookies.set("use_path_argument","value",null, null, null, null, "Lax");
|
||||
```
|
||||
|
||||
#### other operation
|
||||
```
|
||||
// check a cookie exist
|
||||
this.$cookies.isKey("token")
|
||||
|
||||
// get a cookie
|
||||
this.$cookies.get("token");
|
||||
|
||||
// remove a cookie
|
||||
this.$cookies.remove("token");
|
||||
|
||||
// get all cookie key names, line shows
|
||||
this.$cookies.keys().join("\n");
|
||||
|
||||
// remove all cookie
|
||||
this.$cookies.keys().forEach(cookie => this.$cookies.remove(cookie))
|
||||
|
||||
// vue-cookies global
|
||||
[this | Vue].$cookies.[method]
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Warning
|
||||
|
||||
**$cookies key names Cannot be set to ['expires','max-age','path','domain','secure','SameSite']**
|
||||
|
||||
|
||||
## License
|
||||
|
||||
[MIT](http://opensource.org/licenses/MIT)
|
||||
Copyright (c) 2016-present, cmp-cc
|
29
node_modules/vue-cookies/package.json
generated
vendored
Normal file
29
node_modules/vue-cookies/package.json
generated
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "vue-cookies",
|
||||
"version": "1.7.4",
|
||||
"description": "A simple Vue.js plugin for handling browser cookies",
|
||||
"main": "vue-cookies.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/cmp-cc/vue-cookies.git"
|
||||
},
|
||||
"keywords":[
|
||||
"javascript",
|
||||
"vue",
|
||||
"cookie",
|
||||
"cookies",
|
||||
"vue-cookies",
|
||||
"browser",
|
||||
"session"
|
||||
],
|
||||
"author": "cmp-cc",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/cmp-cc/vue-cookies/issues"
|
||||
},
|
||||
"homepage": "https://github.com/cmp-cc/vue-cookies#readme",
|
||||
"typings": "types/index.d.ts"
|
||||
}
|
51
node_modules/vue-cookies/sample/welcome.html
generated
vendored
Normal file
51
node_modules/vue-cookies/sample/welcome.html
generated
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script src="https://unpkg.com/vue/dist/vue.js"></script>
|
||||
<script src="../vue-cookies.js"></script>
|
||||
<title>Welcome Username</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="v-main">
|
||||
|
||||
<p v-if="!welcomeValue">
|
||||
Please enter your name : <input type="text" @keyup.enter="username">
|
||||
</p>
|
||||
<p v-else>
|
||||
Welcome again : {{ welcomeValue }}
|
||||
<button @click="deleteUser">{{deleteUserText}}</button>
|
||||
{{deleteUserState}}
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
<script>
|
||||
|
||||
new Vue({
|
||||
el:'#v-main',
|
||||
data: function() {
|
||||
return {
|
||||
welcomeValue: this.$cookies.get('username'),
|
||||
deleteUserText : 'Delete Cookie',
|
||||
deleteUserState:''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
username : function(event){
|
||||
this.welcomeValue = event.target.value;
|
||||
this.$cookies.set('username', this.welcomeValue)
|
||||
},
|
||||
deleteUser: function(){
|
||||
this.$cookies.remove('username');
|
||||
this.deleteUserState = '√'
|
||||
|
||||
setTimeout(function(){
|
||||
location.reload()
|
||||
}, 0.5 * 1000)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
</html>
|
44
node_modules/vue-cookies/types/index.d.ts
generated
vendored
Normal file
44
node_modules/vue-cookies/types/index.d.ts
generated
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
import _Vue from 'vue';
|
||||
import './vue';
|
||||
|
||||
export declare function install(Vue: typeof _Vue): void;
|
||||
|
||||
export interface VueCookies {
|
||||
/**
|
||||
* Set global config
|
||||
*/
|
||||
config(expireTimes: string | number | Date, path?: string, domain?: string, secure?: boolean, sameSite?: string): void;
|
||||
|
||||
/**
|
||||
* Set a cookie
|
||||
*/
|
||||
set(keyName: string, value: any, expireTimes?: string | number | Date,
|
||||
path?: string, domain?: string, secure?: boolean, sameSite?: string): this;
|
||||
|
||||
/**
|
||||
* Get a cookie
|
||||
*/
|
||||
get(keyName: string): any;
|
||||
|
||||
/**
|
||||
* Remove a cookie
|
||||
*/
|
||||
remove(keyName: string, path?: string, domain?: string): this;
|
||||
|
||||
/**
|
||||
* Exist a cookie name
|
||||
*/
|
||||
isKey(keyName: string): boolean;
|
||||
|
||||
/**
|
||||
* Get All cookie name
|
||||
*/
|
||||
keys(): string[];
|
||||
}
|
||||
|
||||
declare const _default : {
|
||||
VueCookies: VueCookies;
|
||||
install: typeof install;
|
||||
};
|
||||
|
||||
export default _default;
|
11
node_modules/vue-cookies/types/vue.d.ts
generated
vendored
Normal file
11
node_modules/vue-cookies/types/vue.d.ts
generated
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
import { VueCookies } from "./index";
|
||||
|
||||
declare module "vue/types/vue" {
|
||||
interface Vue {
|
||||
$cookies: VueCookies;
|
||||
}
|
||||
|
||||
interface VueConstructor {
|
||||
$cookies: VueCookies;
|
||||
}
|
||||
}
|
146
node_modules/vue-cookies/vue-cookies.js
generated
vendored
Normal file
146
node_modules/vue-cookies/vue-cookies.js
generated
vendored
Normal file
@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Vue Cookies v1.7.4
|
||||
* https://github.com/cmp-cc/vue-cookies
|
||||
*
|
||||
* Copyright 2016, cmp-cc
|
||||
* Released under the MIT license
|
||||
*/
|
||||
|
||||
(function () {
|
||||
|
||||
var defaultConfig = {
|
||||
expires: '1d',
|
||||
path: '; path=/',
|
||||
domain: '',
|
||||
secure: '',
|
||||
sameSite: '; SameSite=Lax'
|
||||
};
|
||||
|
||||
var VueCookies = {
|
||||
// install of Vue
|
||||
install: function (Vue) {
|
||||
Vue.prototype.$cookies = this;
|
||||
Vue.$cookies = this;
|
||||
},
|
||||
config: function (expireTimes, path, domain, secure, sameSite) {
|
||||
defaultConfig.expires = expireTimes ? expireTimes : '1d';
|
||||
defaultConfig.path = path ? '; path=' + path : '; path=/';
|
||||
defaultConfig.domain = domain ? '; domain=' + domain : '';
|
||||
defaultConfig.secure = secure ? '; Secure' : '';
|
||||
defaultConfig.sameSite = sameSite ? '; SameSite=' + sameSite : '; SameSite=Lax';
|
||||
},
|
||||
get: function (key) {
|
||||
var value = decodeURIComponent(document.cookie.replace(new RegExp('(?:(?:^|.*;)\\s*' + encodeURIComponent(key).replace(/[\-\.\+\*]/g, '\\$&') + '\\s*\\=\\s*([^;]*).*$)|^.*$'), '$1')) || null;
|
||||
|
||||
if (value && value.substring(0, 1) === '{' && value.substring(value.length - 1, value.length) === '}') {
|
||||
try {
|
||||
value = JSON.parse(value);
|
||||
} catch (e) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
set: function (key, value, expireTimes, path, domain, secure, sameSite) {
|
||||
if (!key) {
|
||||
throw new Error('Cookie name is not find in first argument.');
|
||||
} else if (/^(?:expires|max\-age|path|domain|secure|SameSite)$/i.test(key)) {
|
||||
throw new Error('Cookie key name illegality, Cannot be set to ["expires","max-age","path","domain","secure","SameSite"]\t current key name: ' + key);
|
||||
}
|
||||
// support json object
|
||||
if (value && value.constructor === Object) {
|
||||
value = JSON.stringify(value);
|
||||
}
|
||||
var _expires = '';
|
||||
expireTimes = expireTimes == undefined ? defaultConfig.expires : expireTimes;
|
||||
if (expireTimes && expireTimes != 0) {
|
||||
switch (expireTimes.constructor) {
|
||||
case Number:
|
||||
if (expireTimes === Infinity || expireTimes === -1) _expires = '; expires=Fri, 31 Dec 9999 23:59:59 GMT';
|
||||
else _expires = '; max-age=' + expireTimes;
|
||||
break;
|
||||
case String:
|
||||
if (/^(?:\d+(y|m|d|h|min|s))$/i.test(expireTimes)) {
|
||||
// get capture number group
|
||||
var _expireTime = expireTimes.replace(/^(\d+)(?:y|m|d|h|min|s)$/i, '$1');
|
||||
// get capture type group , to lower case
|
||||
switch (expireTimes.replace(/^(?:\d+)(y|m|d|h|min|s)$/i, '$1').toLowerCase()) {
|
||||
// Frequency sorting
|
||||
case 'm':
|
||||
_expires = '; max-age=' + +_expireTime * 2592000;
|
||||
break; // 60 * 60 * 24 * 30
|
||||
case 'd':
|
||||
_expires = '; max-age=' + +_expireTime * 86400;
|
||||
break; // 60 * 60 * 24
|
||||
case 'h':
|
||||
_expires = '; max-age=' + +_expireTime * 3600;
|
||||
break; // 60 * 60
|
||||
case 'min':
|
||||
_expires = '; max-age=' + +_expireTime * 60;
|
||||
break; // 60
|
||||
case 's':
|
||||
_expires = '; max-age=' + _expireTime;
|
||||
break;
|
||||
case 'y':
|
||||
_expires = '; max-age=' + +_expireTime * 31104000;
|
||||
break; // 60 * 60 * 24 * 30 * 12
|
||||
default:
|
||||
new Error('unknown exception of "set operation"');
|
||||
}
|
||||
} else {
|
||||
_expires = '; expires=' + expireTimes;
|
||||
}
|
||||
break;
|
||||
case Date:
|
||||
_expires = '; expires=' + expireTimes.toUTCString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
document.cookie =
|
||||
encodeURIComponent(key) + '=' + encodeURIComponent(value) +
|
||||
_expires +
|
||||
(domain ? '; domain=' + domain : defaultConfig.domain) +
|
||||
(path ? '; path=' + path : defaultConfig.path) +
|
||||
(secure == undefined ? defaultConfig.secure : secure ? '; Secure' : '') +
|
||||
(sameSite == undefined ? defaultConfig.sameSite : (sameSite ? '; SameSite=' + sameSite : ''));
|
||||
return this;
|
||||
},
|
||||
remove: function (key, path, domain) {
|
||||
if (!key || !this.isKey(key)) {
|
||||
return false;
|
||||
}
|
||||
document.cookie = encodeURIComponent(key) +
|
||||
'=; expires=Thu, 01 Jan 1970 00:00:00 GMT' +
|
||||
(domain ? '; domain=' + domain : defaultConfig.domain) +
|
||||
(path ? '; path=' + path : defaultConfig.path) +
|
||||
'; SameSite=Lax';
|
||||
return this;
|
||||
},
|
||||
isKey: function (key) {
|
||||
return (new RegExp('(?:^|;\\s*)' + encodeURIComponent(key).replace(/[\-\.\+\*]/g, '\\$&') + '\\s*\\=')).test(document.cookie);
|
||||
},
|
||||
keys: function () {
|
||||
if (!document.cookie) return [];
|
||||
var _keys = document.cookie.replace(/((?:^|\s*;)[^\=]+)(?=;|$)|^\s*|\s*(?:\=[^;]*)?(?:\1|$)/g, '').split(/\s*(?:\=[^;]*)?;\s*/);
|
||||
for (var _index = 0; _index < _keys.length; _index++) {
|
||||
_keys[_index] = decodeURIComponent(_keys[_index]);
|
||||
}
|
||||
return _keys;
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof exports == 'object') {
|
||||
module.exports = VueCookies;
|
||||
} else if (typeof define == 'function' && define.amd) {
|
||||
define([], function () {
|
||||
return VueCookies;
|
||||
});
|
||||
} else if (window.Vue) {
|
||||
Vue.use(VueCookies);
|
||||
}
|
||||
// vue-cookies can exist independently,no dependencies library
|
||||
if (typeof window !== 'undefined') {
|
||||
window.$cookies = VueCookies;
|
||||
}
|
||||
|
||||
})();
|
5
package.json
Normal file
5
package.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"vue-cookies": "^1.7.4"
|
||||
}
|
||||
}
|
@ -13,8 +13,10 @@ import ast
|
||||
import os
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from dotenv import load_dotenv
|
||||
from webpack_loader.loader import WebpackLoader
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
@ -192,6 +194,18 @@ DATABASES = {
|
||||
# Vue webpack settings
|
||||
VUE_DIR = os.path.join(BASE_DIR, 'vue')
|
||||
|
||||
|
||||
class CustomWebpackLoader(WebpackLoader):
|
||||
|
||||
def get_chunk_url(self, chunk):
|
||||
asset = self.get_assets()['assets'][chunk['name']]
|
||||
return super().get_chunk_url(asset)
|
||||
|
||||
def filter_chunks(self, chunks):
|
||||
chunks = [chunk if isinstance(chunk, dict) else {'name': chunk} for chunk in chunks]
|
||||
return super().filter_chunks(chunks)
|
||||
|
||||
|
||||
WEBPACK_LOADER = {
|
||||
'DEFAULT': {
|
||||
'CACHE': not DEBUG,
|
||||
@ -199,7 +213,8 @@ WEBPACK_LOADER = {
|
||||
'STATS_FILE': os.path.join(VUE_DIR, 'webpack-stats.json'),
|
||||
'POLL_INTERVAL': 0.1,
|
||||
'TIMEOUT': None,
|
||||
'IGNORE': [r'.+\.hot-update.js', r'.+\.map']
|
||||
'IGNORE': [r'.+\.hot-update.js', r'.+\.map'],
|
||||
'LOADER_CLASS': 'recipes.settings.CustomWebpackLoader',
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@ Django==3.2
|
||||
cryptography==3.4.7
|
||||
django-annoying==0.10.6
|
||||
django-autocomplete-light==3.8.2
|
||||
django-cleanup==5.1.0
|
||||
django-cleanup==5.2.0
|
||||
django-crispy-forms==1.11.2
|
||||
django-emoji-picker==0.0.6
|
||||
django-filter==2.4.0
|
||||
@ -16,7 +16,7 @@ lxml==4.6.3
|
||||
Markdown==3.3.4
|
||||
Pillow==8.2.0
|
||||
psycopg2-binary==2.8.6
|
||||
python-dotenv==0.17.0
|
||||
python-dotenv==0.17.1
|
||||
requests==2.25.1
|
||||
simplejson==3.17.2
|
||||
six==1.15.0
|
||||
@ -31,7 +31,7 @@ Jinja2==2.11.3
|
||||
django-webpack-loader==0.7.0
|
||||
django-js-reverse==0.9.1
|
||||
django-allauth==0.44.0
|
||||
recipe-scrapers==12.2.2
|
||||
recipe-scrapers==13.1.1
|
||||
django-scopes==1.2.0
|
||||
pytest==6.2.3
|
||||
pytest-django==4.2.0
|
||||
|
@ -14,11 +14,13 @@
|
||||
"moment": "^2.29.1",
|
||||
"vue": "^2.6.11",
|
||||
"vue-class-component": "^7.2.3",
|
||||
"vue-cookies": "^1.7.4",
|
||||
"vue-i18n": "^8.24.3",
|
||||
"vue-multiselect": "^2.1.6",
|
||||
"vue-property-decorator": "^9.1.2",
|
||||
"vue-template-compiler": "^2.6.12",
|
||||
"vuex": "^3.6.0"
|
||||
"vuex": "^3.6.0",
|
||||
"workbox-webpack-plugin": "^6.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kazupon/vue-i18n-loader": "^0.5.0",
|
||||
@ -32,11 +34,11 @@
|
||||
"@vue/compiler-sfc": "^3.0.0",
|
||||
"@vue/eslint-config-typescript": "^7.0.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint": "^7.25.0",
|
||||
"eslint-plugin-vue": "^7.0.0-0",
|
||||
"typescript": "~4.1.5",
|
||||
"typescript": "~4.2.4",
|
||||
"vue-cli-plugin-i18n": "^2.1.0",
|
||||
"webpack-bundle-tracker": "0.4.3",
|
||||
"webpack-bundle-tracker": "1.0.0-alpha.1",
|
||||
"workbox-expiration": "^6.0.2",
|
||||
"workbox-navigation-preload": "^6.0.2",
|
||||
"workbox-precaching": "^6.0.2",
|
||||
|
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<div id="app" style="margin-bottom: 4vh">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xl-2 d-none d-xl-block">
|
||||
<div class="col-md-2 d-none d-md-block">
|
||||
|
||||
</div>
|
||||
<div class="col-xl-8 col-12">
|
||||
@ -10,27 +10,207 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<b-input class="form-control" v-model="search_input" @keyup="refreshData"></b-input>
|
||||
|
||||
|
||||
<b-input-group class="mt-3">
|
||||
<b-input class="form-control" v-model="search_input" @keyup="refreshData"
|
||||
v-bind:placeholder="$t('Search')"></b-input>
|
||||
<b-input-group-append>
|
||||
<b-button v-b-toggle.collapse_advanced_search variant="primary" class="shadow-none"><i
|
||||
class="fas fa-caret-down" v-if="!settings.advanced_search_visible"></i><i class="fas fa-caret-up"
|
||||
v-if="settings.advanced_search_visible"></i>
|
||||
</b-button>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
|
||||
|
||||
<b-collapse id="collapse_advanced_search" class="mt-2" v-model="settings.advanced_search_visible">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3" style="margin-top: 1vh">
|
||||
<a class="btn btn-primary btn-block text-uppercase"
|
||||
:href="resolveDjangoUrl('new_recipe')">{{ $t('New_Recipe') }}</a>
|
||||
</div>
|
||||
<div class="col-md-3" style="margin-top: 1vh">
|
||||
<a class="btn btn-primary btn-block text-uppercase"
|
||||
:href="resolveDjangoUrl('data_import_url')">{{ $t('Url_Import') }}</a>
|
||||
</div>
|
||||
<div class="col-md-3" style="margin-top: 1vh">
|
||||
<button class="btn btn-primary btn-block text-uppercase" @click="resetSearch">
|
||||
{{ $t('Reset_Search') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-2" style="position: relative; margin-top: 1vh">
|
||||
<b-form-checkbox v-model="search_internal" name="check-button" @change="refreshData"
|
||||
class="shadow-none"
|
||||
style="position:relative;top: 50%; transform: translateY(-50%);" switch>
|
||||
{{ $t('show_only_internal') }}
|
||||
</b-form-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="col-md-1" style="position: relative; margin-top: 1vh">
|
||||
<button id="id_settings_button" class="btn btn-primary btn-block"><i class="fas fa-cog"></i></button>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<b-popover
|
||||
target="id_settings_button"
|
||||
triggers="click"
|
||||
placement="bottom"
|
||||
:title="$t('Settings')">
|
||||
<div>
|
||||
<b-form-group
|
||||
v-bind:label="$t('Recently_Viewed')"
|
||||
label-for="popover-input-1"
|
||||
label-cols="6"
|
||||
class="mb-3">
|
||||
<b-form-input
|
||||
type="number"
|
||||
v-model="settings.recently_viewed"
|
||||
id="popover-input-1"
|
||||
size="sm"
|
||||
></b-form-input>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group
|
||||
v-bind:label="$t('Meal_Plan')"
|
||||
label-for="popover-input-2"
|
||||
label-cols="6"
|
||||
class="mb-3">
|
||||
<b-form-checkbox
|
||||
switch
|
||||
v-model="settings.show_meal_plan"
|
||||
id="popover-input-2"
|
||||
size="sm"
|
||||
></b-form-checkbox>
|
||||
</b-form-group>
|
||||
</div>
|
||||
<div class="row" style="margin-top: 1vh">
|
||||
<div class="col-12" style="text-align: right">
|
||||
<b-button size="sm" variant="secondary" style="margin-right:8px" @click="$root.$emit('bv::hide::popover')">{{$t('Close')}}</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</b-popover>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<b-input-group style="margin-top: 1vh">
|
||||
<generic-multiselect @change="genericSelectChanged" parent_variable="search_keywords"
|
||||
:initial_selection="search_keywords"
|
||||
search_function="listKeywords" label="label"
|
||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||
v-bind:placeholder="$t('Keywords')"></generic-multiselect>
|
||||
<b-input-group-append>
|
||||
<b-input-group-text>
|
||||
<b-form-checkbox v-model="settings.search_keywords_or" name="check-button"
|
||||
@change="refreshData"
|
||||
class="shadow-none" switch>
|
||||
<span class="text-uppercase" v-if="settings.search_keywords_or">{{ $t('or') }}</span>
|
||||
<span class="text-uppercase" v-else>{{ $t('and') }}</span>
|
||||
</b-form-checkbox>
|
||||
</b-input-group-text>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<b-input-group style="margin-top: 1vh">
|
||||
<generic-multiselect @change="genericSelectChanged" parent_variable="search_foods"
|
||||
:initial_selection="search_foods"
|
||||
search_function="listFoods" label="name"
|
||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||
v-bind:placeholder="$t('Ingredients')"></generic-multiselect>
|
||||
<b-input-group-append>
|
||||
<b-input-group-text>
|
||||
<b-form-checkbox v-model="settings.search_foods_or" name="check-button"
|
||||
@change="refreshData"
|
||||
class="shadow-none" switch>
|
||||
<span class="text-uppercase" v-if="settings.search_foods_or">{{ $t('or') }}</span>
|
||||
<span class="text-uppercase" v-else>{{ $t('and') }}</span>
|
||||
</b-form-checkbox>
|
||||
</b-input-group-text>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<b-input-group style="margin-top: 1vh">
|
||||
<generic-multiselect @change="genericSelectChanged" parent_variable="search_books"
|
||||
:initial_selection="search_books"
|
||||
search_function="listRecipeBooks" label="name"
|
||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||
v-bind:placeholder="$t('Books')"></generic-multiselect>
|
||||
<b-input-group-append>
|
||||
<b-input-group-text>
|
||||
<b-form-checkbox v-model="settings.search_books_or" name="check-button"
|
||||
@change="refreshData"
|
||||
class="shadow-none" tyle="width: 100%" switch>
|
||||
<span class="text-uppercase" v-if="settings.search_books_or">{{ $t('or') }}</span>
|
||||
<span class="text-uppercase" v-else>{{ $t('and') }}</span>
|
||||
</b-form-checkbox>
|
||||
</b-input-group-text>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</b-collapse>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top: 2vh">
|
||||
<div class="col col-md-12">
|
||||
<b-card-group deck>
|
||||
<recipe-card style="max-width: 15vw; height: 30vh" v-for="r in recipes" v-bind:key="r.id" :recipe="r"></recipe-card>
|
||||
</b-card-group>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));grid-gap: 1rem;">
|
||||
|
||||
<template
|
||||
v-if="search_input === '' && search_keywords.length === 0 && search_foods.length === 0 && search_books.length === 0">
|
||||
<recipe-card v-bind:key="`mp_${m.id}`" v-for="m in meal_plans" :recipe="m.recipe"
|
||||
:meal_plan="m" :footer_text="m.meal_type_name"
|
||||
footer_icon="far fa-calendar-alt"></recipe-card>
|
||||
|
||||
<recipe-card v-for="r in last_viewed_recipes" v-bind:key="`rv_${r.id}`" :recipe="r"
|
||||
v-bind:footer_text="$t('Recently_Viewed')" footer_icon="fas fa-eye"></recipe-card>
|
||||
</template>
|
||||
|
||||
<recipe-card v-for="r in recipes" v-bind:key="r.id" :recipe="r"></recipe-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col-xl-2 d-none d-xl-block">
|
||||
<div class="row" style="margin-top: 2vh">
|
||||
<div class="col col-md-12">
|
||||
<b-pagination
|
||||
v-model="pagination_page"
|
||||
:total-rows="pagination_count"
|
||||
per-page="25"
|
||||
@change="pageChange"
|
||||
align="center">
|
||||
|
||||
</b-pagination>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="col-md-2 d-none d-md-block">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</template>
|
||||
@ -40,6 +220,11 @@ import Vue from 'vue'
|
||||
import {BootstrapVue} from 'bootstrap-vue'
|
||||
|
||||
import 'bootstrap-vue/dist/bootstrap-vue.css'
|
||||
import moment from 'moment'
|
||||
|
||||
import VueCookies from 'vue-cookies'
|
||||
|
||||
Vue.use(VueCookies)
|
||||
|
||||
import {ResolveUrlMixin} from "@/utils/utils";
|
||||
|
||||
@ -47,37 +232,149 @@ import LoadingSpinner from "@/components/LoadingSpinner";
|
||||
|
||||
import {ApiApiFactory} from "@/utils/openapi/api.ts";
|
||||
import RecipeCard from "@/components/RecipeCard";
|
||||
import GenericMultiselect from "@/components/GenericMultiselect";
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
export default {
|
||||
name: 'RecipeSearchView',
|
||||
mixins: [],
|
||||
components: {RecipeCard},
|
||||
mixins: [ResolveUrlMixin],
|
||||
components: {GenericMultiselect, RecipeCard},
|
||||
data() {
|
||||
return {
|
||||
recipes: [],
|
||||
meal_plans: [],
|
||||
last_viewed_recipes: [],
|
||||
|
||||
search_input: '',
|
||||
search_internal: false,
|
||||
search_keywords: [],
|
||||
search_foods: [],
|
||||
search_books: [],
|
||||
|
||||
settings: {
|
||||
search_keywords_or: true,
|
||||
search_foods_or: true,
|
||||
search_books_or: true,
|
||||
advanced_search_visible: false,
|
||||
show_meal_plan: true,
|
||||
recently_viewed: 5,
|
||||
},
|
||||
|
||||
pagination_count: 0,
|
||||
pagination_page: 1,
|
||||
}
|
||||
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(function () {
|
||||
if (this.$cookies.isKey('search_settings')) {
|
||||
console.log('loaded cookie settings', this.$cookies.get("search_settings"))
|
||||
this.settings = this.$cookies.get("search_settings")
|
||||
}
|
||||
|
||||
this.loadMealPlan()
|
||||
this.loadRecentlyViewed()
|
||||
})
|
||||
|
||||
this.refreshData()
|
||||
},
|
||||
watch: {
|
||||
settings: {
|
||||
handler() {
|
||||
this.$cookies.set("search_settings", this.settings, -1)
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
'settings.show_meal_plan': function () {
|
||||
console.log('Test')
|
||||
this.loadMealPlan()
|
||||
},
|
||||
'settings.recently_viewed': function () {
|
||||
console.log('RV')
|
||||
this.loadRecentlyViewed()
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
refreshData: function () {
|
||||
let apiClient = new ApiApiFactory()
|
||||
|
||||
apiClient.listRecipes({query: {query: this.search_input, limit: 10}}).then(result => {
|
||||
this.recipes = result.data
|
||||
apiClient.listRecipes({
|
||||
query: {
|
||||
query: this.search_input,
|
||||
keywords: this.search_keywords.map(function (A) {
|
||||
return A["id"];
|
||||
}),
|
||||
foods: this.search_foods.map(function (A) {
|
||||
return A["id"];
|
||||
}),
|
||||
books: this.search_books.map(function (A) {
|
||||
return A["id"];
|
||||
}),
|
||||
|
||||
keywords_or: this.settings.search_keywords_or,
|
||||
foods_or: this.settings.search_foods_or,
|
||||
books_or: this.settings.search_books_or,
|
||||
|
||||
internal: this.search_internal,
|
||||
page: this.pagination_page,
|
||||
}
|
||||
}).then(result => {
|
||||
this.recipes = result.data.results
|
||||
this.pagination_count = result.data.count
|
||||
})
|
||||
},
|
||||
loadMealPlan: function () {
|
||||
let apiClient = new ApiApiFactory()
|
||||
|
||||
if (this.settings.show_meal_plan) {
|
||||
apiClient.listMealPlans({
|
||||
query: {
|
||||
from_date: moment().format('YYYY-MM-DD'),
|
||||
to_date: moment().format('YYYY-MM-DD')
|
||||
}
|
||||
}).then(result => {
|
||||
this.meal_plans = result.data
|
||||
})
|
||||
} else {
|
||||
this.meal_plans = []
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
loadRecentlyViewed: function () {
|
||||
let apiClient = new ApiApiFactory()
|
||||
if (this.settings.recently_viewed > 0) {
|
||||
apiClient.listRecipes({query: {last_viewed: this.settings.recently_viewed}}).then(result => {
|
||||
this.last_viewed_recipes = result.data.results
|
||||
})
|
||||
} else {
|
||||
this.last_viewed_recipes = []
|
||||
}
|
||||
},
|
||||
genericSelectChanged: function (obj) {
|
||||
this[obj.var] = obj.val
|
||||
this.refreshData()
|
||||
},
|
||||
resetSearch: function () {
|
||||
this.search_input = ''
|
||||
this.search_internal = false
|
||||
this.search_keywords = []
|
||||
this.search_foods = []
|
||||
this.search_books = []
|
||||
this.refreshData()
|
||||
},
|
||||
pageChange: function (page) {
|
||||
this.pagination_page = page
|
||||
this.refreshData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
|
||||
|
||||
<style>
|
||||
|
||||
|
||||
</style>
|
||||
|
66
vue/src/components/GenericMultiselect.vue
Normal file
66
vue/src/components/GenericMultiselect.vue
Normal file
@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<multiselect
|
||||
v-model="selected_objects"
|
||||
:options="objects"
|
||||
:close-on-select="true"
|
||||
:clear-on-select="true"
|
||||
:hide-selected="true"
|
||||
:preserve-search="true"
|
||||
:placeholder="placeholder"
|
||||
:label="label"
|
||||
track-by="id"
|
||||
:multiple="true"
|
||||
:loading="loading"
|
||||
@search-change="search"
|
||||
@input="selectionChanged">
|
||||
</multiselect>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Multiselect from 'vue-multiselect'
|
||||
import {ApiApiFactory} from "@/utils/openapi/api";
|
||||
|
||||
export default {
|
||||
name: "GenericMultiselect",
|
||||
components: {Multiselect},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
objects: [],
|
||||
selected_objects: [],
|
||||
}
|
||||
},
|
||||
props: {
|
||||
placeholder: String,
|
||||
search_function: String,
|
||||
label: String,
|
||||
parent_variable: String,
|
||||
initial_selection: Array,
|
||||
},
|
||||
watch: {
|
||||
initial_selection: function (newVal, oldVal) { // watch it
|
||||
this.selected_objects = newVal
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.search('')
|
||||
},
|
||||
methods: {
|
||||
search: function (query) {
|
||||
let apiClient = new ApiApiFactory()
|
||||
|
||||
apiClient[this.search_function]({query: {query: query, limit: 10}}).then(result => {
|
||||
this.objects = result.data
|
||||
})
|
||||
},
|
||||
selectionChanged: function () {
|
||||
this.$emit('change', {var: this.parent_variable, val: this.selected_objects})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div v-if="recipe.keywords.length > 0">
|
||||
<small :key="k.id" v-for="k in recipe.keywords" style="padding: 2px">
|
||||
{{k.icon}} {{k.name}}
|
||||
<b-badge pill variant="light">{{k.label}}</b-badge>
|
||||
</small>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -1,31 +1,81 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
|
||||
|
||||
<b-card no-body>
|
||||
<a :href="clickUrl()">
|
||||
<b-card-img-lazy style="height: 15vh; object-fit: cover" :src=recipe_image v-bind:alt="$t('Recipe_Image')"
|
||||
top></b-card-img-lazy>
|
||||
|
||||
<b-card-img-lazy :src=recipe.image v-bind:alt="$t('Recipe_Image')" top></b-card-img-lazy>
|
||||
<div class="card-img-overlay h-100 d-flex flex-column justify-content-right"
|
||||
style="float:right; text-align: right; padding-top: 10px; padding-right: 5px">
|
||||
<recipe-context-menu :recipe="recipe" style="float:right" v-if="recipe !== null"></recipe-context-menu>
|
||||
</div>
|
||||
|
||||
<b-card-body :title=recipe.name>
|
||||
<recipe-context-menu :recipe="recipe"></recipe-context-menu>
|
||||
<b-card-text>
|
||||
</a>
|
||||
|
||||
<b-card-body>
|
||||
<h5><a :href="clickUrl()">
|
||||
<template v-if="recipe !== null">{{ recipe.name }}</template>
|
||||
<template v-else>{{ meal_plan.title }}</template>
|
||||
</a></h5>
|
||||
|
||||
<b-card-text style="text-overflow: ellipsis">
|
||||
<template v-if="recipe !== null">
|
||||
{{ recipe.description }}
|
||||
<keywords :recipe="recipe" style="margin-top: 4px"></keywords>
|
||||
<b-badge pill variant="info" v-if="!recipe.internal">{{ $t('External') }}</b-badge>
|
||||
</template>
|
||||
<template v-else>{{ meal_plan.note }}</template>
|
||||
</b-card-text>
|
||||
</b-card-body>
|
||||
|
||||
|
||||
<b-card-footer v-if="footer_text !== undefined">
|
||||
<i v-bind:class="footer_icon"></i> {{ footer_text }}
|
||||
</b-card-footer>
|
||||
</b-card>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import RecipeContextMenu from "@/components/RecipeContextMenu";
|
||||
import Keywords from "@/components/Keywords";
|
||||
import {resolveDjangoUrl, ResolveUrlMixin} from "@/utils/utils";
|
||||
|
||||
export default {
|
||||
name: "RecipeCard",
|
||||
components: {RecipeContextMenu},
|
||||
mixins: [
|
||||
ResolveUrlMixin,
|
||||
],
|
||||
components: {Keywords, RecipeContextMenu},
|
||||
props: {
|
||||
recipe: Object,
|
||||
meal_plan: Object,
|
||||
footer_text: String,
|
||||
footer_icon: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
recipe_image: '',
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
if (this.recipe == null || this.recipe.image === null) {
|
||||
this.recipe_image = window.IMAGE_PLACEHOLDER
|
||||
} else {
|
||||
this.recipe_image = this.recipe.image
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clickUrl: function () {
|
||||
if (this.recipe !== null) {
|
||||
return resolveDjangoUrl('view_recipe', this.recipe.id)
|
||||
} else {
|
||||
return resolveDjangoUrl('view_plan_entry', this.meal_plan.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -19,5 +19,34 @@
|
||||
"Fats": "Fette",
|
||||
"Carbohydrates": "Kohlenhydrate",
|
||||
"Calories": "Kalorien",
|
||||
"Nutrition": "Nährwerte"
|
||||
"Nutrition": "Nährwerte",
|
||||
"Keywords": "Stichwörter",
|
||||
"Books": "Bücher",
|
||||
"show_only_internal": "Nur interne Rezepte anzeigen",
|
||||
"Ingredients": "Zutaten",
|
||||
"min": "Min",
|
||||
"Servings": "Portionen",
|
||||
"Waiting": "Wartezeit",
|
||||
"Preparation": "Zubereitung",
|
||||
"Edit": "Bearbeiten",
|
||||
"Open": "Öffnen",
|
||||
"Save": "Speichern",
|
||||
"Step": "Schritt",
|
||||
"Search": "Suchen",
|
||||
"Print": "Drucken",
|
||||
"New_Recipe": "Neues Rezept",
|
||||
"Url_Import": "URL Import",
|
||||
"Reset_Search": "Suche zurücksetzen",
|
||||
"or": "oder",
|
||||
"and": "und",
|
||||
"Recently_Viewed": "Kürzlich angesehen",
|
||||
"External": "Extern",
|
||||
"Settings": "Einstellungen",
|
||||
"Meal_Plan": "Speiseplan",
|
||||
"Date": "Datum",
|
||||
"Share": "Teilen",
|
||||
"Export": "Exportieren",
|
||||
"Rating": "Bewertung",
|
||||
"Close": "Schließen",
|
||||
"Add": "Hinzufügen"
|
||||
}
|
||||
|
@ -2,6 +2,9 @@
|
||||
"import_running": "Import running, please wait!",
|
||||
"all_fields_optional": "All fields are optional and can be left empty.",
|
||||
"convert_internal": "Convert to internal recipe",
|
||||
"show_only_internal": "Show only internal recipes",
|
||||
|
||||
|
||||
|
||||
"Log_Recipe_Cooking": "Log Recipe Cooking",
|
||||
"External_Recipe_Image": "External Recipe Image",
|
||||
@ -10,13 +13,19 @@
|
||||
"Add_to_Plan": "Add to Plan",
|
||||
"Step_start_time": "Step start time",
|
||||
|
||||
"Meal_Plan": "Meal Plan",
|
||||
"Select_Book": "Select Book",
|
||||
"Recipe_Image": "Recipe Image",
|
||||
"Import_finished": "Import finished",
|
||||
"View_Recipes": "View Recipes",
|
||||
"Log_Cooking": "Log Cooking",
|
||||
"New_Recipe": "New Recipe",
|
||||
"Url_Import": "Url Import",
|
||||
"Reset_Search": "Reset Search",
|
||||
"Recently_Viewed": "Recently Viewed",
|
||||
|
||||
|
||||
"Keywords": "Keywords",
|
||||
"Books": "Books",
|
||||
"Proteins": "Proteins",
|
||||
"Fats": "Fats",
|
||||
"Carbohydrates": "Carbohydrates",
|
||||
@ -33,6 +42,7 @@
|
||||
"Servings": "Servings",
|
||||
"Waiting": "Waiting",
|
||||
"Preparation": "Preparation",
|
||||
"External": "External",
|
||||
"Edit": "Edit",
|
||||
"Open": "Open",
|
||||
"Save": "Save",
|
||||
@ -40,5 +50,8 @@
|
||||
"Search": "Search",
|
||||
"Import": "Import",
|
||||
"Print": "Print",
|
||||
"Settings": "Settings",
|
||||
"or": "or",
|
||||
"and": "and",
|
||||
"Information": "Information"
|
||||
}
|
@ -13,28 +13,36 @@
|
||||
"Import_finished": "Importeren gereed",
|
||||
"View_Recipes": "Bekijk Recepten",
|
||||
"Log_Cooking": "Log Bereiding",
|
||||
"Proteins": "Proteïnes",
|
||||
"Proteins": "Eiwitten",
|
||||
"Fats": "Vetten",
|
||||
"Carbohydrates": "Koolhydraten",
|
||||
"Calories": "Calorieën",
|
||||
"Nutrition": "Voedingswaarde",
|
||||
"Date": "Datum",
|
||||
"Share": "Deel",
|
||||
"Export": "Exporteer",
|
||||
"Rating": "Waardering",
|
||||
"Close": "Sluit",
|
||||
"Export": "Exporteren",
|
||||
"Rating": "Beoordeling",
|
||||
"Close": "Sluiten",
|
||||
"Add": "Voeg toe",
|
||||
"Ingredients": "Ingrediënten",
|
||||
"min": "min",
|
||||
"Servings": "Porties",
|
||||
"Waiting": "Wachten",
|
||||
"Preparation": "Bereiding",
|
||||
"Edit": "Bewerk",
|
||||
"Edit": "Bewerken",
|
||||
"Open": "Open",
|
||||
"Save": "Sla op",
|
||||
"Save": "Opslaan",
|
||||
"Step": "Stap",
|
||||
"Search": "Zoeken",
|
||||
"Import": "Importeer",
|
||||
"Print": "Afdrukken",
|
||||
"Information": "Informatie"
|
||||
"Information": "Informatie",
|
||||
"Keywords": "Etiketten",
|
||||
"Books": "Boeken",
|
||||
"show_only_internal": "Toon alleen interne recepten",
|
||||
"New_Recipe": "Nieuw Recept",
|
||||
"Url_Import": "Importeer URL",
|
||||
"Reset_Search": "Zoeken resetten",
|
||||
"or": "of",
|
||||
"and": "en"
|
||||
}
|
||||
|
@ -78,9 +78,7 @@ module.exports = {
|
||||
})
|
||||
*/
|
||||
|
||||
config
|
||||
.plugin('BundleTracker')
|
||||
.use(BundleTracker, [{filename: '../vue/webpack-stats.json'}]);
|
||||
config.plugin('BundleTracker').use(BundleTracker, [{relativePath: true, path: '../vue/'}]);
|
||||
|
||||
config.resolve.alias
|
||||
.set('__STATIC__', 'static')
|
||||
|
@ -1 +1 @@
|
||||
{"status":"done","chunks":{"chunk-vendors":[{"name":"css/chunk-vendors.css","path":"F:\\Developement\\Django\\recipes\\cookbook\\static\\vue\\css\\chunk-vendors.css"},{"name":"js/chunk-vendors.js","path":"F:\\Developement\\Django\\recipes\\cookbook\\static\\vue\\js\\chunk-vendors.js"}],"import_response_view":[{"name":"js/import_response_view.js","path":"F:\\Developement\\Django\\recipes\\cookbook\\static\\vue\\js\\import_response_view.js"}],"offline_view":[{"name":"js/offline_view.js","path":"F:\\Developement\\Django\\recipes\\cookbook\\static\\vue\\js\\offline_view.js"}],"recipe_search_view":[{"name":"js/recipe_search_view.js","path":"F:\\Developement\\Django\\recipes\\cookbook\\static\\vue\\js\\recipe_search_view.js"}],"recipe_view":[{"name":"js/recipe_view.js","path":"F:\\Developement\\Django\\recipes\\cookbook\\static\\vue\\js\\recipe_view.js"}]}}
|
||||
{"status":"done","chunks":{"recipe_search_view":["css/chunk-vendors.css","js/chunk-vendors.js","js/recipe_search_view.js"],"recipe_view":["css/chunk-vendors.css","js/chunk-vendors.js","js/recipe_view.js"],"offline_view":["css/chunk-vendors.css","js/chunk-vendors.js","js/offline_view.js"],"import_response_view":["css/chunk-vendors.css","js/chunk-vendors.js","js/import_response_view.js"]},"assets":{"../../templates/sw.js":{"name":"../../templates/sw.js","path":"..\\..\\templates\\sw.js"},"css/chunk-vendors.css":{"name":"css/chunk-vendors.css","path":"css\\chunk-vendors.css"},"js/chunk-vendors.js":{"name":"js/chunk-vendors.js","path":"js\\chunk-vendors.js"},"js/import_response_view.js":{"name":"js/import_response_view.js","path":"js\\import_response_view.js"},"js/offline_view.js":{"name":"js/offline_view.js","path":"js\\offline_view.js"},"js/recipe_search_view.js":{"name":"js/recipe_search_view.js","path":"js\\recipe_search_view.js"},"js/recipe_view.js":{"name":"js/recipe_view.js","path":"js\\recipe_view.js"},"recipe_search_view.html":{"name":"recipe_search_view.html","path":"recipe_search_view.html"},"recipe_view.html":{"name":"recipe_view.html","path":"recipe_view.html"},"offline_view.html":{"name":"offline_view.html","path":"offline_view.html"},"import_response_view.html":{"name":"import_response_view.html","path":"import_response_view.html"},"manifest.json":{"name":"manifest.json","path":"manifest.json"}}}
|
1821
vue/yarn.lock
1821
vue/yarn.lock
File diff suppressed because it is too large
Load Diff
8
yarn.lock
Normal file
8
yarn.lock
Normal file
@ -0,0 +1,8 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
vue-cookies@^1.7.4:
|
||||
version "1.7.4"
|
||||
resolved "https://registry.yarnpkg.com/vue-cookies/-/vue-cookies-1.7.4.tgz#d241d0a0431da0795837651d10b4d73e7c8d3e8d"
|
||||
integrity sha512-mOS5Btr8V9zvAtkmQ7/TfqJIropOx7etDAgBywPCmHjvfJl2gFbH2XgoMghleLoyyMTi5eaJss0mPN7arMoslA==
|
Reference in New Issue
Block a user