Merge branch 'pr/865' into develop

This commit is contained in:
vabene1111
2021-09-08 09:42:16 +02:00
44 changed files with 791 additions and 1260 deletions

View File

@@ -61,12 +61,12 @@ with scopes_disabled():
model = Recipe
fields = ['name', 'keywords', 'foods', 'internal']
class FoodFilter(django_filters.FilterSet):
name = django_filters.CharFilter(lookup_expr='icontains')
# class FoodFilter(django_filters.FilterSet):
# name = django_filters.CharFilter(lookup_expr='icontains')
class Meta:
model = Food
fields = ['name']
# class Meta:
# model = Food
# fields = ['name']
class ShoppingListFilter(django_filters.FilterSet):

View File

@@ -6,12 +6,11 @@ from django.utils.translation import gettext_lazy as _
from django_scopes import scopes_disabled
from django_scopes.forms import SafeModelChoiceField, SafeModelMultipleChoiceField
from emoji_picker.widgets import EmojiPickerTextInput
from treebeard.forms import MoveNodeForm
from hcaptcha.fields import hCaptchaField
from .models import (Comment, Food, InviteLink, Keyword, MealPlan, Recipe,
RecipeBook, RecipeBookEntry, Storage, Sync, Unit, User,
UserPreference, SupermarketCategory, MealType, Space,
UserPreference, MealType, Space,
SearchPreference)
@@ -219,31 +218,31 @@ class CommentForm(forms.ModelForm):
}
class KeywordForm(MoveNodeForm):
class Meta:
model = Keyword
fields = ('name', 'icon', 'description')
exclude = ('sib_order', 'parent', 'path', 'depth', 'numchild')
widgets = {'icon': EmojiPickerTextInput}
# class KeywordForm(MoveNodeForm):
# class Meta:
# model = Keyword
# fields = ('name', 'icon', 'description')
# exclude = ('sib_order', 'parent', 'path', 'depth', 'numchild')
# widgets = {'icon': EmojiPickerTextInput}
class FoodForm(forms.ModelForm):
# class FoodForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
space = kwargs.pop('space')
super().__init__(*args, **kwargs)
self.fields['recipe'].queryset = Recipe.objects.filter(space=space).all()
self.fields['supermarket_category'].queryset = SupermarketCategory.objects.filter(space=space).all()
# def __init__(self, *args, **kwargs):
# space = kwargs.pop('space')
# super().__init__(*args, **kwargs)
# self.fields['recipe'].queryset = Recipe.objects.filter(space=space).all()
# self.fields['supermarket_category'].queryset = SupermarketCategory.objects.filter(space=space).all()
class Meta:
model = Food
fields = ('name', 'description', 'ignore_shopping', 'recipe', 'supermarket_category')
widgets = {'recipe': SelectWidget}
# class Meta:
# model = Food
# fields = ('name', 'description', 'ignore_shopping', 'recipe', 'supermarket_category')
# widgets = {'recipe': SelectWidget}
field_classes = {
'recipe': SafeModelChoiceField,
'supermarket_category': SafeModelChoiceField,
}
# field_classes = {
# 'recipe': SafeModelChoiceField,
# 'supermarket_category': SafeModelChoiceField,
# }
class StorageForm(forms.ModelForm):

View File

@@ -20,6 +20,7 @@ def search_recipes(request, queryset, params):
search_keywords = params.getlist('keywords', [])
search_foods = params.getlist('foods', [])
search_books = params.getlist('books', [])
search_units = params.get('units', None)
# TODO I think default behavior should be 'AND' which is how most sites operate with facet/filters based on results
search_keywords_or = params.get('keywords_or', True)
@@ -159,6 +160,10 @@ def search_recipes(request, queryset, params):
else:
for k in search_books:
queryset = queryset.filter(recipebookentry__book__id=k)
# probably only useful in Unit list view, so keeping it simple
if search_units:
queryset = queryset.filter(steps__ingredients__unit__id=search_units)
if search_internal == 'true':
queryset = queryset.filter(internal=True)

View File

@@ -24,6 +24,11 @@ class RecipeSchema(AutoSchema):
"description": 'Id of food a recipe should have. For multiple repeat parameter.',
'schema': {'type': 'string', },
})
parameters.append({
"name": 'units', "in": "query", "required": False,
"description": 'Id of unit a recipe should have.',
'schema': {'type': 'int', },
})
parameters.append({
"name": 'books', "in": "query", "required": False,
"description": 'Id of book a recipe should have. For multiple repeat parameter.',
@@ -85,3 +90,18 @@ class TreeSchema(AutoSchema):
'schema': {'type': 'int', },
})
return parameters
class FilterSchema(AutoSchema):
def get_path_parameters(self, path, method):
if not is_list_view(path, method, self.view):
return super(FilterSchema, self).get_path_parameters(path, method)
api_name = path.split('/')[2]
parameters = super().get_path_parameters(path, method)
parameters.append({
"name": 'query', "in": "query", "required": False,
"description": 'Query string matched against {} name.'.format(api_name),
'schema': {'type': 'string', },
})
return parameters

View File

@@ -118,8 +118,10 @@ class UserFileSerializer(serializers.ModelSerializer):
except TypeError:
current_file_size_mb = 0
if (validated_data['file'].size / 1000 / 1000 + current_file_size_mb - 5) > self.context[
'request'].space.max_file_storage_mb != 0:
if (
(validated_data['file'].size / 1000 / 1000 + current_file_size_mb - 5)
> self.context['request'].space.max_file_storage_mb != 0
):
raise ValidationError(_('You have reached your file upload limit.'))
def create(self, validated_data):
@@ -241,10 +243,24 @@ class KeywordSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
class UnitSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
image = serializers.SerializerMethodField('get_image')
numrecipe = serializers.SerializerMethodField('count_recipes')
def get_image(self, obj):
recipes = Recipe.objects.filter(steps__ingredients__unit=obj, space=obj.space).exclude(image__isnull=True).exclude(image__exact='')
if len(recipes) != 0:
return random.choice(recipes).image.url
else:
return None
def count_recipes(self, obj):
return Recipe.objects.filter(steps__ingredients__unit=obj, space=obj.space).count()
def create(self, validated_data):
obj, created = Unit.objects.get_or_create(name=validated_data['name'].strip(),
space=self.context['request'].space)
validated_data['name'] = validated_data['name'].strip()
validated_data['space'] = self.context['request'].space
obj, created = Unit.objects.get_or_create(**validated_data)
return obj
def update(self, instance, validated_data):
@@ -253,8 +269,8 @@ class UnitSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
class Meta:
model = Unit
fields = ('id', 'name', 'description')
read_only_fields = ('id',)
fields = ('id', 'name', 'description', 'numrecipe', 'image')
read_only_fields = ('id', 'numrecipe', 'image')
class SupermarketCategorySerializer(UniqueFieldsMixin, WritableNestedModelSerializer):

View File

@@ -1 +0,0 @@
.shake[data-v-d394ab04]{-webkit-animation:shake-data-v-d394ab04 .82s cubic-bezier(.36,.07,.19,.97) both;animation:shake-data-v-d394ab04 .82s cubic-bezier(.36,.07,.19,.97) both;transform:translateZ(0);-webkit-backface-visibility:hidden;backface-visibility:hidden;perspective:1000px}@-webkit-keyframes shake-data-v-d394ab04{10%,90%{transform:translate3d(-1px,0,0)}20%,80%{transform:translate3d(2px,0,0)}30%,50%,70%{transform:translate3d(-4px,0,0)}40%,60%{transform:translate3d(4px,0,0)}}@keyframes shake-data-v-d394ab04{10%,90%{transform:translate3d(-1px,0,0)}20%,80%{transform:translate3d(2px,0,0)}30%,50%,70%{transform:translate3d(-4px,0,0)}40%,60%{transform:translate3d(4px,0,0)}}

View File

@@ -1 +0,0 @@
.shake[data-v-d394ab04]{-webkit-animation:shake-data-v-d394ab04 .82s cubic-bezier(.36,.07,.19,.97) both;animation:shake-data-v-d394ab04 .82s cubic-bezier(.36,.07,.19,.97) both;transform:translateZ(0);-webkit-backface-visibility:hidden;backface-visibility:hidden;perspective:1000px}@-webkit-keyframes shake-data-v-d394ab04{10%,90%{transform:translate3d(-1px,0,0)}20%,80%{transform:translate3d(2px,0,0)}30%,50%,70%{transform:translate3d(-4px,0,0)}40%,60%{transform:translate3d(4px,0,0)}}@keyframes shake-data-v-d394ab04{10%,90%{transform:translate3d(-1px,0,0)}20%,80%{transform:translate3d(2px,0,0)}30%,50%,70%{transform:translate3d(-4px,0,0)}40%,60%{transform:translate3d(4px,0,0)}}

View File

@@ -0,0 +1 @@
.shake[data-v-021606fa]{-webkit-animation:shake-data-v-021606fa .82s cubic-bezier(.36,.07,.19,.97) both;animation:shake-data-v-021606fa .82s cubic-bezier(.36,.07,.19,.97) both;transform:translateZ(0);-webkit-backface-visibility:hidden;backface-visibility:hidden;perspective:1000px}@-webkit-keyframes shake-data-v-021606fa{10%,90%{transform:translate3d(-1px,0,0)}20%,80%{transform:translate3d(2px,0,0)}30%,50%,70%{transform:translate3d(-4px,0,0)}40%,60%{transform:translate3d(4px,0,0)}}@keyframes shake-data-v-021606fa{10%,90%{transform:translate3d(-1px,0,0)}20%,80%{transform:translate3d(2px,0,0)}30%,50%,70%{transform:translate3d(-4px,0,0)}40%,60%{transform:translate3d(4px,0,0)}}

View File

@@ -1 +0,0 @@
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Vue App</title><link href="css/chunk-vendors.css" rel="preload" as="style"><link href="css/food_list_view.css" rel="preload" as="style"><link href="js/chunk-vendors.js" rel="preload" as="script"><link href="js/food_list_view.js" rel="preload" as="script"><link href="css/chunk-vendors.css" rel="stylesheet"><link rel="icon" type="image/png" sizes="32x32" href="img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="img/icons/favicon-16x16.png"><link rel="manifest" href="manifest.json"><meta name="theme-color" content="#4DBA87"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black"><meta name="apple-mobile-web-app-title" content="Recipes"><link rel="apple-touch-icon" href="img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="img/icons/safari-pinned-tab.svg" color="#4DBA87"><meta name="msapplication-TileImage" content="img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><div id="app"></div><script src="js/chunk-vendors.js"></script></body></html>

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Vue App</title><link href="css/chunk-vendors.css" rel="preload" as="style"><link href="css/keyword_list_view.css" rel="preload" as="style"><link href="js/chunk-vendors.js" rel="preload" as="script"><link href="js/keyword_list_view.js" rel="preload" as="script"><link href="css/chunk-vendors.css" rel="stylesheet"><link rel="icon" type="image/png" sizes="32x32" href="img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="img/icons/favicon-16x16.png"><link rel="manifest" href="manifest.json"><meta name="theme-color" content="#4DBA87"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black"><meta name="apple-mobile-web-app-title" content="Recipes"><link rel="apple-touch-icon" href="img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="img/icons/safari-pinned-tab.svg" color="#4DBA87"><meta name="msapplication-TileImage" content="img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><div id="app"></div><script src="js/chunk-vendors.js"></script></body></html>

View File

@@ -0,0 +1 @@
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Vue App</title><link href="css/chunk-vendors.css" rel="preload" as="style"><link href="css/model_list_view.css" rel="preload" as="style"><link href="js/chunk-vendors.js" rel="preload" as="script"><link href="js/model_list_view.js" rel="preload" as="script"><link href="css/chunk-vendors.css" rel="stylesheet"><link rel="icon" type="image/png" sizes="32x32" href="img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="img/icons/favicon-16x16.png"><link rel="manifest" href="manifest.json"><meta name="theme-color" content="#4DBA87"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black"><meta name="apple-mobile-web-app-title" content="Recipes"><link rel="apple-touch-icon" href="img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="img/icons/safari-pinned-tab.svg" color="#4DBA87"><meta name="msapplication-TileImage" content="img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><div id="app"></div><script src="js/chunk-vendors.js"></script></body></html>

View File

@@ -52,13 +52,13 @@ class RecipeTable(tables.Table):
)
class IngredientTable(tables.Table):
id = tables.LinkColumn('edit_food', args=[A('id')])
# class IngredientTable(tables.Table):
# id = tables.LinkColumn('edit_food', args=[A('id')])
class Meta:
model = Keyword
template_name = 'generic/table_template.html'
fields = ('id', 'name')
# class Meta:
# model = Keyword
# template_name = 'generic/table_template.html'
# fields = ('id', 'name')
class StorageTable(tables.Table):

View File

@@ -97,6 +97,9 @@
<a class="dropdown-item" href="{% url 'list_food' %}"><i
class="fas fa-leaf fa-fw"></i> {% trans 'Ingredients' %}
</a>
<a class="dropdown-item" href="{% url 'list_unit' %}"><i
class="fas fa-balance-scale fa-fw"></i> {% trans 'Units' %}
</a>
<a class="dropdown-item" href="{% url 'view_supermarket' %}"><i
class="fas fa-store-alt fa-fw"></i> {% trans 'Supermarket' %}
</a>
@@ -114,6 +117,28 @@
class="fas fa-edit fa-fw"></i> {% trans 'Batch Edit' %}</a>
</div>
</li>
{% comment %} should food and units be moved to the keyword dropdown and rename that?
feels like the original intent of the organization of these menus has shifted maybe something like this?{% endcomment %}
<li class="nav-item dropdown {% if request.resolver_match.url_name in 'list_food,list_unit,list_keyword,data_batch_edit' %}active{% endif %}">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<i class="fas fa-blender"></i> {% trans 'Gadgets' %}
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
<a class="dropdown-item" href="{% url 'list_food' %}"><i
class="fas fa-leaf fa-fw"></i> {% trans 'Ingredients' %}
</a>
<a class="dropdown-item" href="{% url 'list_unit' %}"><i
class="fas fa-balance-scale fa-fw"></i> {% trans 'Units' %}
</a>
<a class="dropdown-item" href="{% url 'list_keyword' %}"><i
class="fas fa-tags fa-fw"></i> {% trans 'Keyword' %}</a>
{% comment %} does this funcionatliy need moved to the keyword list page?
the Recipe Search might be better? {% endcomment %}
<a class="dropdown-item" href="{% url 'data_batch_edit' %}"><i
class="fas fa-edit fa-fw"></i> {% trans 'Batch Edit' %}</a>
</div>
</li>
<li class="nav-item dropdown {% if request.resolver_match.url_name in 'list_storage,data_sync,list_recipe_import,list_sync_log,data_stats,edit_food,edit_storage' %}active{% endif %}">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false"><i class="fas fa-database"></i> {% trans 'Storage Data' %}
@@ -129,8 +154,8 @@
class="fas fa-history fa-fw"></i> {% trans 'Discovery Log' %}</a>
<a class="dropdown-item" href="{% url 'data_stats' %}"><i
class="fas fa-chart-line fa-fw"></i> {% trans 'Statistics' %}</a>
<a class="dropdown-item" href="{% url 'edit_food' %}"><i
class="fas fa-balance-scale fa-fw"></i> {% trans 'Units & Ingredients' %}</a>
{% comment %} <a class="dropdown-item" href="{% url 'edit_food' %}"><i
class="fas fa-balance-scale fa-fw"></i> {% trans 'Units & Ingredients' %}</a> {% endcomment %}
<a class="dropdown-item" href="{% url 'data_import_url' %}"><i
class="fas fa-file-import"></i> {% trans 'Import Recipe' %}</a>

View File

@@ -766,7 +766,7 @@
searchUnits: function (query) {
this.units_loading = true
this.$http.get("{% url 'api:unit-list' %}" + '?query=' + query + '&limit=10').then((response) => {
this.units = response.data;
this.units = response.data.results;
if (this.recipe !== undefined) {
for (let s of this.recipe.steps) {

View File

@@ -2,9 +2,8 @@
{% load render_bundle from webpack_loader %}
{% load static %}
{% load i18n %}
{% load l10n %}
{% comment %} TODO Can this be combined with Keyword template? {% endcomment %}
{% block title %}{% trans 'Food' %}{% endblock %}
{% comment %} {% load l10n %} {% endcomment %}
{% block title %}{{ title }}{% endblock %}
{% block content_fluid %}
@@ -17,6 +16,8 @@
{% block script %}
{{ config | json_script:"model_config" }}
{% if debug %}
<script src="{% url 'js_reverse' %}"></script>
{% else %}
@@ -27,5 +28,5 @@
window.IMAGE_PLACEHOLDER = "{% static 'assets/recipe_no_image.svg' %}"
</script>
{% render_bundle 'food_list_view' %}
{% render_bundle 'model_list_view' %}
{% endblock %}

View File

@@ -1,31 +0,0 @@
{% extends "base.html" %}
{% load render_bundle from webpack_loader %}
{% load static %}
{% load i18n %}
{% load l10n %}
{% comment %} TODO Can this be combined with Food template? {% endcomment %}
{% block title %}{% trans 'Keywords' %}{% endblock %}
{% block content_fluid %}
<div id="app" >
<keyword-list-view></keyword-list-view>
</div>
{% endblock %}
{% block script %}
{% if debug %}
<script src="{% url 'js_reverse' %}"></script>
{% else %}
<script src="{% static 'django_js_reverse/reverse.js' %}"></script>
{% endif %}
<script type="application/javascript">
window.IMAGE_PLACEHOLDER = "{% static 'assets/recipe_no_image.svg' %}"
</script>
{% render_bundle 'keyword_list_view' %}
{% endblock %}

View File

@@ -908,7 +908,7 @@
searchKeywords: function (query) {
this.keywords_loading = true
this.$http.get("{% url 'api:keyword-list' %}" + '?query=' + query + '&limit=10').then((response) => {
this.keywords = response.data;
this.keywords = response.data.results;
this.keywords_loading = false
}).catch((err) => {
console.log(err)
@@ -919,7 +919,7 @@
searchUnits: function (query) { //TODO move to central component
this.units_loading = true
this.$http.get("{% url 'api:unit-list' %}" + '?query=' + query + '&limit=10').then((response) => {
this.units = response.data;
this.units = response.data.results;
this.units_loading = false
}).catch((err) => {
this.makeToast(gettext('Error'), gettext("There was an error loading a resource!") + err.bodyText, 'danger')
@@ -928,7 +928,7 @@
searchFoods: function (query) { //TODO move to central component
this.foods_loading = true
this.$http.get("{% url 'api:food-list' %}" + '?query=' + query + '&limit=10').then((response) => {
this.foods = response.data
this.foods = response.data.results
this.foods_loading = false
}).catch((err) => {
this.makeToast(gettext('Error'), gettext("There was an error loading a resource!") + err.bodyText, 'danger')

View File

@@ -273,7 +273,7 @@ def test_integrity(u1_s1, recipe_1_s1):
)
)
assert r.status_code == 204
with scopes_disabled():
assert Food.objects.count() == 9
assert Ingredient.objects.count() == 9
@@ -327,7 +327,6 @@ def test_move(u1_s1, obj_1, obj_1_1, obj_1_1_1, obj_2, obj_3, space_1):
assert Food.find_problems() == ([], [], [], [], [])
# this seems overly long - should it be broken into smaller pieces?
def test_merge(
u1_s1,
obj_1, obj_1_1, obj_1_1_1, obj_2, obj_3,

View File

@@ -1,13 +1,20 @@
import json
import pytest
import uuid
from django.contrib import auth
from django.urls import reverse
from django_scopes import scopes_disabled
from cookbook.models import Food, Unit
from cookbook.models import Food, Ingredient, ShoppingList, ShoppingListEntry, Unit
LIST_URL = 'api:unit-list'
DETAIL_URL = 'api:unit-detail'
MERGE_URL = 'api:unit-merge'
def random_food(space_1, u1_s1):
return Food.objects.get_or_create(name=str(uuid.uuid4()), space=space_1)[0]
@pytest.fixture()
@@ -20,6 +27,47 @@ def obj_2(space_1):
return Unit.objects.get_or_create(name='test_2', space=space_1)[0]
@pytest.fixture
def obj_3(space_2):
return Unit.objects.get_or_create(name='test_3', space=space_2)[0]
@pytest.fixture()
def ing_1_s1(obj_1, space_1, u1_s1):
return Ingredient.objects.create(unit=obj_1, food=random_food(space_1, u1_s1), space=space_1)
@pytest.fixture()
def ing_2_s1(obj_2, space_1, u1_s1):
return Ingredient.objects.create(unit=obj_2, food=random_food(space_1, u1_s1), space=space_1)
@pytest.fixture()
def ing_3_s2(obj_3, space_2, u2_s2):
return Ingredient.objects.create(unit=obj_3, food=random_food(space_2, u2_s2), space=space_2)
@pytest.fixture()
def sle_1_s1(obj_1, u1_s1, space_1):
e = ShoppingListEntry.objects.create(unit=obj_1, food=random_food(space_1, u1_s1))
s = ShoppingList.objects.create(created_by=auth.get_user(u1_s1), space=space_1, )
s.entries.add(e)
return e
@pytest.fixture()
def sle_2_s1(obj_2, u1_s1, space_1):
return ShoppingListEntry.objects.create(unit=obj_2, food=random_food(space_1, u1_s1))
@pytest.fixture()
def sle_3_s2(obj_3, u2_s2, space_2):
e = ShoppingListEntry.objects.create(unit=obj_3, food=random_food(space_2, u2_s2))
s = ShoppingList.objects.create(created_by=auth.get_user(u2_s2), space=space_2)
s.entries.add(e)
return e
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
@@ -32,33 +80,31 @@ def test_list_permission(arg, request):
def test_list_space(obj_1, obj_2, u1_s1, u1_s2, space_2):
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 2
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0
assert json.loads(u1_s1.get(reverse(LIST_URL)).content)['count'] == 2
assert json.loads(u1_s2.get(reverse(LIST_URL)).content)['count'] == 0
obj_1.space = space_2
obj_1.save()
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 1
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 1
assert json.loads(u1_s1.get(reverse(LIST_URL)).content)['count'] == 1
assert json.loads(u1_s2.get(reverse(LIST_URL)).content)['count'] == 1
def test_list_filter(obj_1, obj_2, u1_s1):
r = u1_s1.get(reverse(LIST_URL))
assert r.status_code == 200
response = json.loads(r.content)
assert len(response) == 2
# unit model isn't sorted - this isn't a stable test
# assert response[0]['name'] == obj_1.name
assert response['count'] == 2
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?limit=1').content)
assert len(response) == 1
assert response['count'] == 1
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query=chicken').content)
assert len(response) == 0
assert response['count'] == 0
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query={obj_1.name[4:]}').content)
assert len(response) == 1
assert response[0]['name'] == obj_1.name
assert response['count'] == 1
assert response['results'][0]['name'] == obj_1.name
@pytest.mark.parametrize("arg", [
@@ -148,3 +194,60 @@ def test_delete(u1_s1, u1_s2, obj_1):
assert r.status_code == 204
with scopes_disabled():
assert Food.objects.count() == 0
def test_merge(
u1_s1,
obj_1, obj_2, obj_3,
ing_1_s1, ing_2_s1, ing_3_s2,
sle_1_s1, sle_2_s1, sle_3_s2,
space_1
):
with scopes_disabled():
assert Unit.objects.filter(space=space_1).count() == 2
assert obj_1.ingredient_set.count() == 1
assert obj_2.ingredient_set.count() == 1
assert obj_3.ingredient_set.count() == 1
assert obj_1.shoppinglistentry_set.count() == 1
assert obj_2.shoppinglistentry_set.count() == 1
assert obj_3.shoppinglistentry_set.count() == 1
# merge Unit with ingredient/shopping list entry with another Unit, only HTTP put method should work
url = reverse(MERGE_URL, args=[obj_1.id, obj_2.id])
r = u1_s1.get(url)
assert r.status_code == 405
r = u1_s1.post(url)
assert r.status_code == 405
r = u1_s1.delete(url)
assert r.status_code == 405
r = u1_s1.put(url)
assert r.status_code == 200
with scopes_disabled():
assert Unit.objects.filter(pk=obj_1.id).count() == 0
assert obj_2.ingredient_set.count() == 2
assert obj_3.ingredient_set.count() == 1
assert obj_2.shoppinglistentry_set.count() == 2
assert obj_3.shoppinglistentry_set.count() == 1
# attempt to merge with non-existent parent
r = u1_s1.put(
reverse(MERGE_URL, args=[obj_2.id, 9999])
)
assert r.status_code == 404
# attempt to move to wrong space
r = u1_s1.put(
reverse(MERGE_URL, args=[obj_2.id, obj_3.id])
)
assert r.status_code == 404
# attempt to merge with self
r = u1_s1.put(
reverse(MERGE_URL, args=[obj_2.id, obj_2.id])
)
assert r.status_code == 403
# run diagnostic to find problems - none should be found
with scopes_disabled():
assert Food.find_problems() == ([], [], [], [], [])

View File

@@ -10,8 +10,8 @@ from cookbook.helper import dal
from .models import (Comment, Food, InviteLink, Keyword, MealPlan, Recipe,
RecipeBook, RecipeBookEntry, RecipeImport, ShoppingList,
Storage, Sync, SyncLog, get_model_name)
from .views import api, data, delete, edit, import_export, lists, trees, new, views, telegram
Storage, Sync, SyncLog, Unit, get_model_name)
from .views import api, data, delete, edit, import_export, lists, new, views, telegram
router = routers.DefaultRouter()
router.register(r'user-name', api.UserNameViewSet, basename='username')
@@ -87,7 +87,7 @@ urlpatterns = [
path('edit/recipe/convert/<int:pk>/', edit.convert_recipe, name='edit_convert_recipe'),
path('edit/storage/<int:pk>/', edit.edit_storage, name='edit_storage'),
path('edit/ingredient/', edit.edit_ingredients, name='edit_food'), # TODO is this still needed?
path('edit/ingredient/', edit.edit_ingredients, name='edit_food'), # TODO deprecate?
path('delete/recipe-source/<int:pk>/', delete.delete_recipe_source, name='delete_recipe_source'),
@@ -176,12 +176,12 @@ for m in generic_models:
)
)
tree_models = [Keyword, Food]
for m in tree_models:
vue_models = [Food, Keyword, Unit]
for m in vue_models:
py_name = get_model_name(m)
url_name = py_name.replace('_', '-')
if c := getattr(trees, py_name, None):
if c := getattr(lists, py_name, None):
urlpatterns.append(
path(
f'list/{url_name}/', c, name=f'list_{py_name}'

View File

@@ -48,7 +48,7 @@ from cookbook.models import (CookLog, Food, Ingredient, Keyword, MealPlan,
from cookbook.provider.dropbox import Dropbox
from cookbook.provider.local import Local
from cookbook.provider.nextcloud import Nextcloud
from cookbook.schemas import RecipeSchema, TreeSchema
from cookbook.schemas import FilterSchema, RecipeSchema, TreeSchema
from cookbook.serializer import (FoodSerializer, IngredientSerializer,
KeywordSerializer, MealPlanSerializer,
MealTypeSerializer, RecipeBookSerializer,
@@ -98,6 +98,8 @@ class DefaultPagination(PageNumberPagination):
class FuzzyFilterMixin(ViewSetMixin):
schema = FilterSchema()
def get_queryset(self):
self.queryset = self.queryset.filter(space=self.request.space)
query = self.request.query_params.get('query', None)
@@ -164,7 +166,12 @@ class MergeMixin(ViewSetMixin): # TODO update Units to use merge API
if target in source.get_descendants_and_self():
content = {'error': True, 'msg': _('Cannot merge with child object!')}
return Response(content, status=status.HTTP_403_FORBIDDEN)
isTree = True
except AttributeError:
# AttributeError probably means its not a tree, so can safely ignore
isTree = False
try:
for link in [field for field in source._meta.get_fields() if issubclass(type(field), ForeignObjectRel)]:
linkManager = getattr(source, link.get_accessor_name())
related = linkManager.all()
@@ -181,9 +188,10 @@ class MergeMixin(ViewSetMixin): # TODO update Units to use merge API
else:
# a new scenario exists and needs to be handled
raise NotImplementedError
children = source.get_children().exclude(id=target.id)
for c in children:
c.move(target, 'sorted-child')
if isTree:
children = source.get_children().exclude(id=target.id)
for c in children:
c.move(target, 'sorted-child')
content = {'msg': _(f'{source.name} was merged successfully with {target.name}')}
source.delete()
return Response(content, status=status.HTTP_200_OK)
@@ -363,14 +371,12 @@ class KeywordViewSet(viewsets.ModelViewSet, TreeMixin):
pagination_class = DefaultPagination
class UnitViewSet(viewsets.ModelViewSet, FuzzyFilterMixin):
class UnitViewSet(viewsets.ModelViewSet, MergeMixin, FuzzyFilterMixin):
queryset = Unit.objects
model = Unit
serializer_class = UnitSerializer
permission_classes = [CustomIsUser]
def get_queryset(self):
self.queryset = self.queryset.filter(space=self.request.space)
return super().get_queryset()
pagination_class = DefaultPagination
class FoodViewSet(viewsets.ModelViewSet, TreeMixin):

View File

@@ -9,7 +9,7 @@ from django.views.generic import DeleteView
from cookbook.helper.permission_helper import (GroupRequiredMixin,
OwnerRequiredMixin,
group_required)
from cookbook.models import (Comment, InviteLink, Keyword, MealPlan, Recipe,
from cookbook.models import (Comment, InviteLink, MealPlan, Recipe,
RecipeBook, RecipeBookEntry, RecipeImport,
Storage, Sync)
from cookbook.provider.dropbox import Dropbox
@@ -73,16 +73,16 @@ class SyncDelete(GroupRequiredMixin, DeleteView):
return context
class KeywordDelete(GroupRequiredMixin, DeleteView):
groups_required = ['user']
template_name = "generic/delete_template.html"
model = Keyword
success_url = reverse_lazy('list_keyword')
# class KeywordDelete(GroupRequiredMixin, DeleteView):
# groups_required = ['user']
# template_name = "generic/delete_template.html"
# model = Keyword
# success_url = reverse_lazy('list_keyword')
def get_context_data(self, **kwargs):
context = super(KeywordDelete, self).get_context_data(**kwargs)
context['title'] = _("Keyword")
return context
# def get_context_data(self, **kwargs):
# context = super(KeywordDelete, self).get_context_data(**kwargs)
# context['title'] = _("Keyword")
# return context
class StorageDelete(GroupRequiredMixin, DeleteView):

View File

@@ -10,14 +10,14 @@ from django.views.generic import UpdateView
from django.views.generic.edit import FormMixin
from django_scopes import scopes_disabled
from cookbook.forms import (CommentForm, ExternalRecipeForm, FoodForm,
FoodMergeForm, KeywordForm, MealPlanForm,
from cookbook.forms import (CommentForm, ExternalRecipeForm,
FoodMergeForm, MealPlanForm,
RecipeBookForm, StorageForm, SyncForm,
UnitMergeForm)
from cookbook.helper.permission_helper import (GroupRequiredMixin,
OwnerRequiredMixin,
group_required)
from cookbook.models import (Comment, Food, Ingredient, Keyword, MealPlan,
from cookbook.models import (Comment, Ingredient, MealPlan,
MealType, Recipe, RecipeBook, RecipeImport,
Storage, Sync, UserPreference)
from cookbook.provider.dropbox import Dropbox
@@ -86,38 +86,38 @@ class SyncUpdate(GroupRequiredMixin, UpdateView, SpaceFormMixing):
return context
class KeywordUpdate(GroupRequiredMixin, UpdateView):
groups_required = ['user']
template_name = "generic/edit_template.html"
model = Keyword
form_class = KeywordForm
# class KeywordUpdate(GroupRequiredMixin, UpdateView):
# groups_required = ['user']
# template_name = "generic/edit_template.html"
# model = Keyword
# form_class = KeywordForm
# TODO add msg box
# # TODO add msg box
def get_success_url(self):
return reverse('list_keyword')
# def get_success_url(self):
# return reverse('list_keyword')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = _("Keyword")
return context
# def get_context_data(self, **kwargs):
# context = super().get_context_data(**kwargs)
# context['title'] = _("Keyword")
# return context
class FoodUpdate(GroupRequiredMixin, UpdateView, SpaceFormMixing):
groups_required = ['user']
template_name = "generic/edit_template.html"
model = Food
form_class = FoodForm
# class FoodUpdate(GroupRequiredMixin, UpdateView, SpaceFormMixing):
# groups_required = ['user']
# template_name = "generic/edit_template.html"
# model = Food
# form_class = FoodForm
# TODO add msg box
# # TODO add msg box
def get_success_url(self):
return reverse('edit_food', kwargs={'pk': self.object.pk})
# def get_success_url(self):
# return reverse('edit_food', kwargs={'pk': self.object.pk})
def get_context_data(self, **kwargs):
context = super(FoodUpdate, self).get_context_data(**kwargs)
context['title'] = _("Food")
return context
# def get_context_data(self, **kwargs):
# context = super(FoodUpdate, self).get_context_data(**kwargs)
# context['title'] = _("Food")
# return context
@group_required('admin')
@@ -279,6 +279,7 @@ class ExternalRecipeUpdate(GroupRequiredMixin, UpdateView, SpaceFormMixing):
return context
# TODO deprecate
@group_required('user')
def edit_ingredients(request):
if request.method == "POST":

View File

@@ -5,11 +5,11 @@ from django.shortcuts import render
from django.utils.translation import gettext as _
from django_tables2 import RequestConfig
from cookbook.filters import FoodFilter, ShoppingListFilter
from cookbook.filters import ShoppingListFilter
from cookbook.helper.permission_helper import group_required
from cookbook.models import (Food, InviteLink, RecipeImport,
from cookbook.models import (InviteLink, RecipeImport,
ShoppingList, Storage, SyncLog)
from cookbook.tables import (ImportLogTable, IngredientTable, InviteLinkTable,
from cookbook.tables import (ImportLogTable, InviteLinkTable,
RecipeImportTable, ShoppingListTable, StorageTable)
@@ -40,18 +40,18 @@ def recipe_import(request):
)
@group_required('user')
def food(request):
f = FoodFilter(request.GET, queryset=Food.objects.filter(space=request.space).all().order_by('pk'))
# @group_required('user')
# def food(request):
# f = FoodFilter(request.GET, queryset=Food.objects.filter(space=request.space).all().order_by('pk'))
table = IngredientTable(f.qs)
RequestConfig(request, paginate={'per_page': 25}).configure(table)
# table = IngredientTable(f.qs)
# RequestConfig(request, paginate={'per_page': 25}).configure(table)
return render(
request,
'generic/list_template.html',
{'title': _("Ingredients"), 'table': table, 'filter': f}
)
# return render(
# request,
# 'generic/list_template.html',
# {'title': _("Ingredients"), 'table': table, 'filter': f}
# )
@group_required('user')
@@ -101,3 +101,52 @@ def invite_link(request):
'table': table,
'create_url': 'new_invite_link'
})
@group_required('user')
def keyword(request):
return render(
request,
'generic/model_template.html',
{
"title": _("Keywords"),
"config": {
'model': "KEYWORD",
'recipe_param': 'keywords'
}
}
)
@group_required('user')
def food(request):
# recipe-param is the name of the parameters used when filtering recipes by this attribute
# model-name is the models.js name of the model, probably ALL-CAPS
return render(
request,
'generic/model_template.html',
{
"title": _("Foods"),
"config": {
'model': "FOOD", # *REQUIRED* name of the model in models.js
'recipe_param': 'foods' # *OPTIONAL* name of the listRecipes parameter if filtering on this attribute
}
}
)
@group_required('user')
def unit(request):
# recipe-param is the name of the parameters used when filtering recipes by this attribute
# model-name is the models.js name of the model, probably ALL-CAPS
return render(
request,
'generic/model_template.html',
{
"title": _("Units"),
"config": {
'model': "UNIT", # *REQUIRED* name of the model in models.js
'recipe_param': 'units' # *OPTIONAL* name of the listRecipes parameter if filtering on this attribute
}
}
)

View File

@@ -12,11 +12,11 @@ from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext as _
from django.views.generic import CreateView
from cookbook.forms import (ImportRecipeForm, InviteLinkForm, KeywordForm,
from cookbook.forms import (ImportRecipeForm, InviteLinkForm,
MealPlanForm, RecipeBookForm, Storage, StorageForm)
from cookbook.helper.permission_helper import (GroupRequiredMixin,
group_required)
from cookbook.models import (InviteLink, Keyword, MealPlan, MealType, Recipe,
from cookbook.models import (InviteLink, MealPlan, MealType, Recipe,
RecipeBook, RecipeImport, ShareLink, Step, UserPreference)
from cookbook.views.edit import SpaceFormMixing
@@ -60,22 +60,22 @@ def share_link(request, pk):
return HttpResponseRedirect(reverse('view_recipe', kwargs={'pk': pk, 'share': link.uuid}))
class KeywordCreate(GroupRequiredMixin, CreateView):
groups_required = ['user']
template_name = "generic/new_template.html"
model = Keyword
form_class = KeywordForm
success_url = reverse_lazy('list_keyword')
# class KeywordCreate(GroupRequiredMixin, CreateView):
# groups_required = ['user']
# template_name = "generic/new_template.html"
# model = Keyword
# form_class = KeywordForm
# success_url = reverse_lazy('list_keyword')
def form_valid(self, form):
form.cleaned_data['space'] = self.request.space
form.save()
return HttpResponseRedirect(reverse('list_keyword'))
# def form_valid(self, form):
# form.cleaned_data['space'] = self.request.space
# form.save()
# return HttpResponseRedirect(reverse('list_keyword'))
def get_context_data(self, **kwargs):
context = super(KeywordCreate, self).get_context_data(**kwargs)
context['title'] = _("Keyword")
return context
# def get_context_data(self, **kwargs):
# context = super(KeywordCreate, self).get_context_data(**kwargs)
# context['title'] = _("Keyword")
# return context
class StorageCreate(GroupRequiredMixin, CreateView):

View File

@@ -1,13 +0,0 @@
from django.shortcuts import render
from cookbook.helper.permission_helper import group_required
@group_required('user')
def keyword(request):
return render(request, 'model/keyword_template.html', {})
@group_required('user')
def food(request):
return render(request, 'model/food_template.html', {})

13
vue/package-lock.json generated
View File

@@ -44,7 +44,7 @@
"eslint-plugin-vue": "^7.10.0",
"typescript": "~4.3.2",
"vue-cli-plugin-i18n": "^2.1.1",
"webpack-bundle-tracker": "1.1.0",
"webpack-bundle-tracker": "1.3.0",
"workbox-expiration": "^6.0.2",
"workbox-navigation-preload": "^6.0.2",
"workbox-precaching": "^6.0.2",
@@ -14289,15 +14289,15 @@
}
},
"node_modules/webpack-bundle-tracker": {
"version": "1.1.0",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/webpack-bundle-tracker/-/webpack-bundle-tracker-1.3.0.tgz",
"integrity": "sha512-cs3QMgW5F0mE0e91X/SuEq2MUfu2LqpjFSdfINsOmNt/T4v39EESNhJ+P8d5lmbfFL6Z1z7n6LlpVCERTdDDvQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"lodash.assign": "^4.2.0",
"lodash.defaults": "^4.2.0",
"lodash.foreach": "^4.5.0",
"lodash.get": "^4.4.2",
"lodash.merge": "^4.6.2",
"strip-ansi": "^6.0.0"
}
},
@@ -24887,14 +24887,15 @@
}
},
"webpack-bundle-tracker": {
"version": "1.1.0",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/webpack-bundle-tracker/-/webpack-bundle-tracker-1.3.0.tgz",
"integrity": "sha512-cs3QMgW5F0mE0e91X/SuEq2MUfu2LqpjFSdfINsOmNt/T4v39EESNhJ+P8d5lmbfFL6Z1z7n6LlpVCERTdDDvQ==",
"dev": true,
"requires": {
"lodash.assign": "^4.2.0",
"lodash.defaults": "^4.2.0",
"lodash.foreach": "^4.5.0",
"lodash.get": "^4.4.2",
"lodash.merge": "^4.6.2",
"strip-ansi": "^6.0.0"
}
},

View File

@@ -1,605 +0,0 @@
<template>
<div id="app" style="margin-bottom: 4vh">
<div class="row">
<div class="col-md-2 d-none d-md-block">
</div>
<div class="col-xl-8 col-12">
<!-- TODO only show scollbars in split mode, but this doesn't interact well with infinite scroll, maybe a different component? -->
<div class="container-fluid d-flex flex-column flex-grow-1" :class="{'vh-100' : show_split}">
<!-- expanded options box -->
<div class="row flex-shrink-0">
<div class="col col-md-12">
<b-collapse id="collapse_advanced" class="mt-2" v-model="advanced_visible">
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-md-3" style="margin-top: 1vh">
<div class="btn btn-primary btn-block text-uppercase" @click="startAction({'action':'new'})">
{{ this.$t('New_Keyword') }}
</div>
</div>
<div class="col-md-3" style="margin-top: 1vh">
<button class="btn btn-primary btn-block text-uppercase" @click="resetSearch">
{{ this.$t('Reset_Search') }}
</button>
</div>
<div class="col-md-3" style="position: relative; margin-top: 1vh">
<b-form-checkbox v-model="show_split" name="check-button"
class="shadow-none"
style="position:relative;top: 50%; transform: translateY(-50%);" switch>
{{ this.$t('show_split_screen') }}
</b-form-checkbox>
</div>
</div>
</div>
</div>
</b-collapse>
</div>
</div>
<div class="row flex-shrink-0">
<!-- search box -->
<div class="col col-md">
<b-input-group class="mt-3">
<b-input class="form-control" v-model="search_input"
v-bind:placeholder="this.$t('Search')"></b-input>
<b-input-group-append>
<b-button v-b-toggle.collapse_advanced variant="primary" class="shadow-none">
<i class="fas fa-caret-down" v-if="!advanced_visible"></i>
<i class="fas fa-caret-up" v-if="advanced_visible"></i>
</b-button>
</b-input-group-append>
</b-input-group>
</div>
<!-- split side search -->
<div class="col col-md" v-if="show_split">
<b-input-group class="mt-3">
<b-input class="form-control" v-model="search_input2"
v-bind:placeholder="this.$t('Search')"></b-input>
</b-input-group>
</div>
</div>
<!-- only show scollbars in split mode, but this doesn't interact well with infinite scroll, maybe a different componenet? -->
<div class="row" :class="{'overflow-hidden' : show_split}" style="margin-top: 2vh">
<div class="col col-md" :class="{'mh-100 overflow-auto' : show_split}">
<generic-horizontal-card v-for="kw in keywords" v-bind:key="kw.id"
:item=kw
item_type="Keyword"
:draggable="true"
:merge="true"
:move="true"
@item-action="startAction($event, 'left')"
/>
<infinite-loading
:identifier='left'
@infinite="infiniteHandler($event, 'left')"
spinner="waveDots">
<template v-slot:no-more><span/></template>
</infinite-loading>
</div>
<!-- right side keyword cards -->
<div class="col col-md mh-100 overflow-auto " v-if="show_split">
<generic-horizontal-card v-for="kw in keywords2" v-bind:key="kw.id"
:item=kw
item_type="Keyword"
:draggable="true"
:merge="true"
:move="true"
@item-action="startAction($event, 'right')"
/>
<infinite-loading
:identifier='right'
@infinite="infiniteHandler($event, 'right')"
spinner="waveDots">
<template v-slot:no-more><span/></template>
</infinite-loading>
</div>
</div>
</div>
</div>
<div class="col-md-2 d-none d-md-block">
</div>
</div>
<!-- TODO Modals can probably be made generic and moved to component -->
<!-- edit modal -->
<b-modal class="modal"
:id="'id_modal_keyword_edit'"
@shown="prepareEmoji"
:title="this.$t('Edit_Keyword')"
:ok-title="this.$t('Save')"
:cancel-title="this.$t('Cancel')"
@ok="saveKeyword">
<form>
<label for="id_keyword_name_edit">{{ this.$t('Name') }}</label>
<input class="form-control" type="text" id="id_keyword_name_edit" v-model="this_item.name">
<label for="id_keyword_description_edit">{{ this.$t('Description') }}</label>
<input class="form-control" type="text" id="id_keyword_description_edit" v-model="this_item.description">
<label for="id_keyword_icon_edit">{{ this.$t('Icon') }}</label>
<twemoji-textarea
id="id_keyword_icon_edit"
ref="_edit"
:emojiData="emojiDataAll"
:emojiGroups="emojiGroups"
triggerType="hover"
recentEmojisFeat="true"
recentEmojisStorage="local"
@contentChanged="setIcon"
/>
</form>
</b-modal>
<!-- delete modal -->
<b-modal class="modal"
:id="'id_modal_keyword_delete'"
:title="this.$t('Delete_Keyword')"
:ok-title="this.$t('Delete')"
:cancel-title="this.$t('Cancel')"
@ok="delKeyword(this_item.id)">
{{this.$t("delete_confimation", {'kw': this_item.name})}} {{this_item.name}}
</b-modal>
<!-- move modal -->
<b-modal class="modal" v-if="models"
:id="'id_modal_keyword_move'"
:title="this.$t('Move_Keyword')"
:ok-title="this.$t('Move')"
:cancel-title="this.$t('Cancel')"
@ok="moveKeyword(this_item.id, this_item.target.id)">
{{ this.$t("move_selection", {'child': this_item.name}) }}
<generic-multiselect
@change="this_item.target=$event.val"
label="name"
:model="models.KEYWORD"
:multiple="false"
:sticky_options="[{'id': 0,'name': $t('Root')}]"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
:placeholder="this.$t('Search')">
</generic-multiselect>
</b-modal>
<!-- merge modal -->
<b-modal class="modal" v-if="models"
:id="'id_modal_keyword_merge'"
:title="this.$t('Merge_Keyword')"
:ok-title="this.$t('Merge')"
:cancel-title="this.$t('Cancel')"
@ok="mergeKeyword(this_item.id, this_item.target.id)">
{{ this.$t("merge_selection", {'source': this_item.name, 'type': this.$t('keyword')}) }}
<generic-multiselect
@change="this_item.target=$event.val"
:model="models.KEYWORD"
:multiple="false"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
:placeholder="this.$t('Search')">
</generic-multiselect>
</b-modal>
</div>
</template>
<script>
import axios from "axios";
axios.defaults.xsrfCookieName = 'csrftoken'
axios.defaults.xsrfHeaderName = "X-CSRFTOKEN"
import Vue from 'vue'
import {BootstrapVue} from 'bootstrap-vue'
import 'bootstrap-vue/dist/bootstrap-vue.css'
import _debounce from 'lodash/debounce'
import {ToastMixin} from "@/utils/utils";
import {ApiApiFactory} from "@/utils/openapi/api.ts";
import GenericHorizontalCard from "@/components/GenericHorizontalCard";
import GenericMultiselect from "@/components/GenericMultiselect";
import InfiniteLoading from 'vue-infinite-loading';
// would move with modals if made generic
import {TwemojiTextarea} from '@kevinfaguiar/vue-twemoji-picker';
// TODO add localization
import EmojiAllData from '@kevinfaguiar/vue-twemoji-picker/emoji-data/en/emoji-all-groups.json';
import EmojiGroups from '@kevinfaguiar/vue-twemoji-picker/emoji-data/emoji-groups.json';
// end move with generic modals
Vue.use(BootstrapVue)
import {Models} from "@/utils/models";
export default {
name: 'KeywordListView',
mixins: [ToastMixin],
components: {TwemojiTextarea, GenericHorizontalCard, GenericMultiselect, InfiniteLoading},
computed: {
// move with generic modals
emojiDataAll() {
return EmojiAllData;
},
emojiGroups() {
return EmojiGroups;
}
// end move with generic modals
},
data() {
return {
keywords: [],
keywords2: [],
models: Models,
show_split: false,
search_input: '',
search_input2: '',
advanced_visible: false,
right_page: 0,
right: +new Date(),
isDirtyRight: false,
left_page: 0,
left: +new Date(),
isDirtyLeft: false,
this_item: {
'id': -1,
'name': '',
'description': '',
'icon': '',
'target': {
'id': -1,
'name': ''
},
},
}
},
watch: {
search_input: _debounce(function() {
this.left_page = 0
this.keywords = []
this.left += 1
}, 700),
search_input2: _debounce(function() {
this.right_page = 0
this.keywords2 = []
this.right += 1
}, 700)
},
methods: {
resetSearch: function () {
if (this.search_input !== '') {
this.search_input = ''
} else {
this.left_page = 0
this.keywords = []
this.left += 1
}
if (this.search_input2 !== '') {
this.search_input2 = ''
} else {
this.right_page = 0
this.keywords2 = []
this.right += 1
}
},
// TODO should model actions be included with the context menu? the card? a seperate mixin avaible to all?
startAction: function(e, col) {
let target = e.target || null
let source = e.source || null
if (e.action == 'delete') {
this.this_item = source
this.$bvModal.show('id_modal_keyword_delete')
} else if (e.action == 'new') {
this.this_item = {}
this.$bvModal.show('id_modal_keyword_edit')
} else if (e.action == 'edit') {
this.this_item = source
this.$bvModal.show('id_modal_keyword_edit')
} else if (e.action === 'move') {
this.this_item = source
if (target == null) {
this.$bvModal.show('id_modal_keyword_move')
} else {
this.moveKeyword(source.id, target.id)
}
} else if (e.action === 'merge') {
this.this_item = source
if (target == null) {
this.$bvModal.show('id_modal_keyword_merge')
} else {
this.mergeKeyword(e.source.id, e.target.id)
}
} else if (e.action === 'get-children') {
if (source.show_children) {
Vue.set(source, 'show_children', false)
} else {
this.this_item = source
this.getChildren(col, source)
}
} else if (e.action === 'get-recipes') {
if (source.show_recipes) {
Vue.set(source, 'show_recipes', false)
} else {
this.this_item = source
this.getRecipes(col, source)
}
}
},
saveKeyword: function () {
let apiClient = new ApiApiFactory()
let kw = {
name: this.this_item.name,
description: this.this_item.description,
icon: this.this_item.icon,
}
if (!this.this_item.id) { // if there is no item id assume its a new item
apiClient.createKeyword(kw).then(result => {
// place all new keywords at the top of the list - could sort instead
this.keywords = [result.data].concat(this.keywords)
// this creates a deep copy to make sure that columns stay independent
if (this.show_split){
this.keywords2 = [JSON.parse(JSON.stringify(result.data))].concat(this.keywords2)
} else {
this.keywords2 = []
}
this.this_item={}
}).catch((err) => {
console.log(err)
this.this_item = {}
})
} else {
apiClient.partialUpdateKeyword(this.this_item.id, kw).then(result => {
this.refreshCard(this.this_item.id)
this.this_item={}
}).catch((err) => {
console.log(err)
this.this_item = {}
})
}
},
delKeyword: function (id) {
let apiClient = new ApiApiFactory()
apiClient.destroyKeyword(id).then(response => {
this.destroyCard(id)
}).catch((err) => {
console.log(err)
this.this_item = {}
})
},
moveKeyword: function (source_id, target_id) {
let apiClient = new ApiApiFactory()
apiClient.moveKeyword(String(source_id), String(target_id)).then(result => {
if (target_id === 0) {
let kw = this.findKeyword(this.keywords, source_id) || this.findKeyword(this.keywords2, source_id)
kw.parent = null
if (this.show_split){
this.destroyCard(source_id) // order matters, destroy old card before adding it back in at root
this.keywords = [kw].concat(this.keywords)
this.keywords2 = [JSON.parse(JSON.stringify(kw))].concat(this.keywords2)
} else {
this.destroyCard(source_id)
this.keywords = [kw].concat(this.keywords)
this.keywords2 = []
}
} else {
this.destroyCard(source_id)
this.refreshCard(target_id)
}
}).catch((err) => {
// TODO none of the error checking works because the openapi generated functions don't throw an error?
// or i'm capturing it incorrectly
console.log(err)
this.makeToast(this.$t('Error'), err.bodyText, 'danger')
})
},
mergeKeyword: function (source_id, target_id) {
let apiClient = new ApiApiFactory()
apiClient.mergeKeyword(String(source_id), String(target_id)).then(result => {
this.destroyCard(source_id)
this.refreshCard(target_id)
}).catch((err) => {
console.log('Error', err)
this.makeToast(this.$t('Error'), err.bodyText, 'danger')
})
},
// TODO: DRY the listKeyword functions (refresh, get children, infinityHandler ) can probably all be consolidated into a single function
getChildren: function(col, kw){
let apiClient = new ApiApiFactory()
let parent = {}
let query = undefined
let page = undefined
let root = kw.id
let tree = undefined
let pageSize = 200
apiClient.listKeywords(query, root, tree, page, pageSize).then(result => {
if (col == 'left') {
parent = this.findKeyword(this.keywords, kw.id)
} else if (col == 'right'){
parent = this.findKeyword(this.keywords2, kw.id)
}
if (parent) {
Vue.set(parent, 'children', result.data.results)
Vue.set(parent, 'show_children', true)
Vue.set(parent, 'show_recipes', false)
}
}).catch((err) => {
console.log(err)
this.makeToast(this.$t('Error'), err.bodyText, 'danger')
})
},
getRecipes: function(col, kw){
let apiClient = new ApiApiFactory()
let parent = {}
let pageSize = 200
let keyword = String(kw.id)
apiClient.listRecipes(
undefined, keyword, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, undefined, undefined, pageSize, undefined
).then(result => {
if (col == 'left') {
parent = this.findKeyword(this.keywords, kw.id)
} else if (col == 'right'){
parent = this.findKeyword(this.keywords2, kw.id)
}
if (parent) {
Vue.set(parent, 'recipes', result.data.results)
Vue.set(parent, 'show_recipes', true)
Vue.set(parent, 'show_children', false)
}
}).catch((err) => {
console.log(err)
this.makeToast(this.$t('Error'), err.bodyText, 'danger')
})
},
refreshCard: function(id){
let target = {}
let apiClient = new ApiApiFactory()
let idx = undefined
let idx2 = undefined
apiClient.retrieveKeyword(id).then(result => {
target = this.findKeyword(this.keywords, id) || this.findKeyword(this.keywords2, id)
if (target.parent) {
let parent = this.findKeyword(this.keywords, target.parent)
let parent2 = this.findKeyword(this.keywords2, target.parent)
if (parent) {
if (parent.show_children){
idx = parent.children.indexOf(parent.children.find(kw => kw.id === target.id))
Vue.set(parent.children, idx, result.data)
}
}
if (parent2){
if (parent2.show_children){
idx2 = parent2.children.indexOf(parent2.children.find(kw => kw.id === target.id))
// deep copy to force columns to be indepedent
Vue.set(parent2.children, idx2, JSON.parse(JSON.stringify(result.data)))
}
}
} else {
idx = this.keywords.indexOf(this.keywords.find(kw => kw.id === target.id))
idx2 = this.keywords2.indexOf(this.keywords2.find(kw => kw.id === target.id))
Vue.set(this.keywords, idx, result.data)
Vue.set(this.keywords2, idx2, JSON.parse(JSON.stringify(result.data)))
}
})
},
findKeyword: function(card_list, id){
let card_length = card_list?.length ?? 0
if (card_length == 0) {
return false
}
let cards = card_list.filter(obj => obj.id == id)
if (cards.length == 1) {
return cards[0]
} else if (cards.length == 0) {
for (const c of card_list.filter(x => x.show_children == true)) {
cards = this.findKeyword(c.children, id)
if (cards) {
return cards
}
}
} else {
console.log('something terrible happened')
}
},
// this would move with modals with mixin?
prepareEmoji: function() {
this.$refs._edit.addText(this.this_item.icon || '');
this.$refs._edit.blur()
document.getElementById('btn-emoji-default').disabled = true;
},
// this would move with modals with mixin?
setIcon: function(icon) {
this.this_item.icon = icon
},
infiniteHandler: function($state, col) {
let apiClient = new ApiApiFactory()
let query = (col==='left') ? this.search_input : this.search_input2
let page = (col==='left') ? this.left_page + 1 : this.right_page + 1
let root = undefined
let tree = undefined
let pageSize = undefined
if (query === '') {
query = undefined
root = 0
}
apiClient.listKeywords(query, root, tree, page, pageSize).then(result => {
if (result.data.results.length){
if (col ==='left') {
this.left_page+=1
this.keywords = this.keywords.concat(result.data.results)
$state.loaded();
if (this.keywords.length >= result.data.count) {
$state.complete();
}
} else if (col ==='right') {
this.right_page+=1
this.keywords2 = this.keywords2.concat(result.data.results)
$state.loaded();
if (this.keywords2.length >= result.data.count) {
$state.complete();
}
}
} else {
console.log('no data returned')
$state.complete();
}
}).catch((err) => {
console.log(err)
this.makeToast(this.$t('Error'), err.bodyText, 'danger')
$state.complete();
})
},
destroyCard: function(id) {
let kw = this.findKeyword(this.keywords, id)
let kw2 = this.findKeyword(this.keywords2, id)
let p_id = undefined
p_id = kw?.parent ?? kw2.parent
if (p_id) {
let parent = this.findKeyword(this.keywords, p_id)
let parent2 = this.findKeyword(this.keywords2, p_id)
if (parent){
Vue.set(parent, 'numchild', parent.numchild - 1)
if (parent.show_children) {
let idx = parent.children.indexOf(parent.children.find(kw => kw.id === id))
Vue.delete(parent.children, idx)
}
}
if (parent2){
Vue.set(parent2, 'numchild', parent2.numchild - 1)
if (parent2.show_children) {
let idx = parent2.children.indexOf(parent2.children.find(kw => kw.id === id))
Vue.delete(parent2.children, idx)
}
}
}
this.keywords = this.keywords.filter(kw => kw.id != id)
this.keywords2 = this.keywords2.filter(kw => kw.id != id)
}
}
}
</script>
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
<style>
</style>

View File

@@ -1,10 +0,0 @@
import Vue from 'vue'
import App from './KeywordListView'
import i18n from '@/i18n'
Vue.config.productionTip = false
new Vue({
i18n,
render: h => h(App),
}).$mount('#app')

View File

@@ -10,6 +10,8 @@
@finish-action="finishAction"/>
<generic-split-lists v-if="this_model"
:list_name="this_model.name"
:right_counts="right_counts"
:left_counts="left_counts"
@reset="resetList"
@get-list="getItems"
@item-action="startAction">
@@ -19,13 +21,18 @@
:item=i
:item_type="this_model.name"
:draggable="true"
:merge="true"
:move="true"
:merge="this_model['merge'] !== false"
:move="this_model['move'] !== false"
@item-action="startAction($event, 'left')">
<!-- foods can also be a recipe, show link to the recipe if it exists -->
<template v-slot:upper-right>
<b-button v-if="i.recipe" v-b-tooltip.hover :title="i.recipe.name"
class=" btn fas fa-book-open p-0 border-0" variant="link" :href="i.recipe.url"/>
<!-- keywords can have icons - if it exists, display it -->
<b-button v-if="i.icon"
class=" btn p-0 border-0" variant="link">
{{i.icon}}
</b-button>
</template>
</generic-horizontal-card>
</template>
@@ -34,8 +41,8 @@
:item=i
:item_type="this_model.name"
:draggable="true"
:merge="true"
:move="true"
:merge="this_model['merge'] !== false"
:move="this_model['move'] !== false"
@item-action="startAction($event, 'right')">
<!-- foods can also be a recipe, show link to the recipe if it exists -->
<template v-slot:upper-right>
@@ -67,7 +74,9 @@ import GenericModalForm from "@/components/Modals/GenericModalForm";
Vue.use(BootstrapVue)
export default {
name: 'FoodListView', // TODO: make generic name
// TODO ApiGenerator doesn't capture and share error information - would be nice to share error details when available
// or i'm capturing it incorrectly
name: 'ModelListView',
mixins: [CardMixin, ToastMixin, ApiMixin],
components: {GenericHorizontalCard, GenericSplitLists, GenericModalForm},
data() {
@@ -75,26 +84,28 @@ export default {
// this.Models and this.Actions inherited from ApiMixin
items_left: [],
items_right: [],
load_more_left: true,
load_more_right: true,
right_counts: {'max': 9999, 'current': 0},
left_counts: {'max': 9999, 'current': 0},
this_model: undefined,
this_action: undefined,
this_recipe_param: undefined,
this_item: {},
this_target: {},
show_modal: false
}
},
mounted() {
this.this_model = this.Models.FOOD //TODO: mounted method to calcuate
// value is passed from lists.py
let model_config = JSON.parse(document.getElementById('model_config').textContent)
this.this_model = this.Models[model_config?.model]
this.this_recipe_param = model_config?.recipe_param
},
methods: {
// this.genericAPI inherited from ApiMixin
resetList: function (e) {
if (e.column === 'left') {
this.items_left = []
} else if (e.column === 'right') {
this.items_right = []
}
this['items_' + e.column] = []
this[e.column + '_counts'].max = 9999 + Math.random()
this[e.column + '_counts'].current = 0
},
startAction: function (e, param) {
let source = e?.source ?? {}
@@ -175,28 +186,18 @@ export default {
}
this.clearState()
},
getItems: function (params, callback) {
getItems: function (params) {
let column = params?.column ?? 'left'
// TODO: does this need to be a callback?
this.genericAPI(this.this_model, this.Actions.LIST, params).then((result) => {
if (result.data.results.length) {
if (column === 'left') {
// if paginated results are in result.data.results otherwise just result.data
this.items_left = this.items_left.concat(result.data?.results ?? result.data)
} else if (column === 'right') {
this.items_right = this.items_right.concat(result.data?.results ?? result.data)
}
// are the total elements less than the length of the array? if so, stop loading
// TODO: generalize this to handle results in result.data
callback(result.data.count > (column === "left" ? this.items_left.length : this.items_right.length))
this['items_' + column] = this['items_' + column].concat(result.data?.results)
this[column + '_counts']['current'] = this['items_' + column].length
this[column + '_counts']['max'] = result.data.count
} else {
callback(false) // stop loading
this[column + '_counts']['current'] = 0
this[column + '_counts']['max'] = 0
console.log('no data returned')
}
// return true if total objects are still less than the length of the list
// TODO this needs generalized to handle non-paginated data
callback(result.data.count < (column === "left" ? this.items_left.length : this.items_right.length))
}).catch((err) => {
console.log(err)
StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
@@ -208,10 +209,11 @@ export default {
saveThis: function (thisItem) {
if (!thisItem?.id) { // if there is no item id assume it's a new item
this.genericAPI(this.this_model, this.Actions.CREATE, thisItem).then((result) => {
// place all new items at the top of the list - could sort instead
this.items_left = [result.data].concat(this.items_left)
// look for and destroy any existing cards to prevent duplicates in the GET case of get_or_create
// then place all new items at the top of the list - could sort instead
this.items_left = [result.data].concat(this.destroyCard(result?.data?.id, this.items_left))
// this creates a deep copy to make sure that columns stay independent
this.items_right = [{...result.data}].concat(this.items_right)
this.items_right = [{...result.data}].concat(this.destroyCard(result?.data?.id, this.items_right))
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
}).catch((err) => {
console.log(err)
@@ -233,14 +235,15 @@ export default {
this.clearState()
return
}
if (source_id === undefined || target_id === undefined) {
let item = this.findCard(source_id, this.items_left) || this.findCard(source_id, this.items_right)
if (source_id === undefined || target_id === undefined || item?.parent == target_id) {
this.makeToast(this.$t('Warning'), this.$t('Nothing to do'), 'warning')
this.clearState()
return
}
this.genericAPI(this.this_model, this.Actions.MOVE, {'source': source_id, 'target': target_id}).then((result) => {
if (target_id === 0) {
let item = this.findCard(source_id, this.items_left) || this.findCard(source_id, this.items_right)
this.items_left = [item].concat(this.destroyCard(source_id, this.items_left)) // order matters, destroy old card before adding it back in at root
this.items_right = [...[item]].concat(this.destroyCard(source_id, this.items_right)) // order matters, destroy old card before adding it back in at root
item.parent = null
@@ -252,8 +255,6 @@ export default {
// TODO make standard toast
this.makeToast(this.$t('Success'), 'Succesfully moved resource', 'success')
}).catch((err) => {
// TODO none of the error checking works because the openapi generated functions don't throw an error?
// or i'm capturing it incorrectly
console.log(err)
this.makeToast(this.$t('Error'), err.bodyText, 'danger')
})
@@ -292,7 +293,7 @@ export default {
'pageSize': 200
}
this.genericAPI(this.this_model, this.Actions.LIST, options).then((result) => {
parent = this.findCard(item.id, col === 'left' ? this.items_left : this.items_right)
parent = this.findCard(item.id, this['items_' + col])
if (parent) {
Vue.set(parent, 'children', result.data.results)
Vue.set(parent, 'show_children', true)
@@ -303,15 +304,14 @@ export default {
this.makeToast(this.$t('Error'), err.bodyText, 'danger')
})
},
getRecipes: function (col, food) {
getRecipes: function (col, item) {
let parent = {}
// TODO: make this generic
let options = {
'foods': food.id,
'pageSize': 200
}
let options = {'pageSize': 200}
options[this.this_recipe_param] = item.id
this.genericAPI(this.Models.RECIPE, this.Actions.LIST, options).then((result) => {
parent = this.findCard(food.id, col === 'left' ? this.items_left : this.items_right)
parent = this.findCard(item.id, this['items_' + col])
if (parent) {
Vue.set(parent, 'recipes', result.data.results)
Vue.set(parent, 'show_recipes', true)

View File

@@ -1,5 +1,5 @@
import Vue from 'vue'
import App from './FoodListView'
import App from './ModelListView'
import i18n from '@/i18n'
Vue.config.productionTip = false

View File

@@ -396,6 +396,7 @@ export default {
this.settings.search_input,
this.settings.search_keywords,
this.settings.search_foods,
undefined,
this.settings.search_books.map(function (A) {
return A["id"];
}),

View File

@@ -19,7 +19,7 @@
<h5 class="m-0 mt-1 text-truncate">{{ item[title] }}</h5>
<div class= "m-0 text-truncate">{{ item[subtitle] }}</div>
<div class="mt-auto mb-1 d-flex flex-row justify-content-end">
<div v-if="item[child_count] !=0" class="mx-2 btn btn-link btn-sm"
<div v-if="item[child_count]" class="mx-2 btn btn-link btn-sm"
style="z-index: 800;" v-on:click="$emit('item-action',{'action':'get-children','source':item})">
<div v-if="!item.show_children">{{ item[child_count] }} {{ item_type }}</div>
<div v-else>{{ text.hide_children }}</div>
@@ -112,6 +112,7 @@ export default {
recipes: {type: String, default: 'recipes'},
move: {type: Boolean, default: false},
merge: {type: Boolean, default: false},
tree: {type: Boolean, default: false},
},
data() {
return {

View File

@@ -64,7 +64,7 @@
</div>
<!-- only show scollbars in split mode -->
<!-- weird behavior when switching to split mode, infinite scoll doesn't trigger if
<!-- TODO: weird behavior when switching to split mode, infinite scoll doesn't trigger if
bottom of page is in viewport can trigger by scrolling page (not column) up -->
<div class="row" :class="{'overflow-hidden' : show_split}">
<div class="col col-md" :class="{'mh-100 overflow-auto' : show_split}">
@@ -74,6 +74,7 @@
@infinite="infiniteHandler($event, 'left')"
spinner="waveDots">
<template v-slot:no-more><span/></template>
<template v-slot:no-results><span>{{$t('No_Results')}}</span></template>
</infinite-loading>
</div>
<!-- right side cards -->
@@ -84,6 +85,7 @@
@infinite="infiniteHandler($event, 'right')"
spinner="waveDots">
<template v-slot:no-more><span/></template>
<template v-slot:no-results><span>{{$t('No_Results')}}</span></template>
</infinite-loading>
</div>
</div>
@@ -96,18 +98,20 @@
</template>
<script>
import Vue from 'vue' // maybe not needed?
import 'bootstrap-vue/dist/bootstrap-vue.css'
import _debounce from 'lodash/debounce'
import InfiniteLoading from 'vue-infinite-loading';
export default {
// TODO: this should be simplified into a Generic Infinitely Scrolling List and added as two components when split lists desired
name: 'GenericSplitLists',
components: {InfiniteLoading},
props: {
list_name: {type: String, default: 'Blank List'}, // TODO update translations to handle plural translations
left_list: {type:Array, default(){return []}},
left_list: {type: Array, default(){return []}},
left_counts: {type: Object},
right_list: {type:Array, default(){return []}},
right_counts: {type: Object},
},
data() {
return {
@@ -117,6 +121,8 @@ export default {
search_left: '',
right_page: 0,
left_page: 0,
right_state: undefined,
left_state: undefined,
right: +new Date(),
left: +new Date(),
text: {
@@ -143,11 +149,45 @@ export default {
this.$emit('reset', {'column':'right'})
this.right += 1
}, 700),
right_counts: {
deep: true,
handler(newVal, oldVal) {
if (newVal.current > 0) {
this.right_state.loaded()
}
if (newVal.current >= newVal.max) {
this.right_state.complete()
}
}
},
left_counts: {
deep: true,
handler(newVal, oldVal) {
if (newVal.current > 0) {
this.left_state.loaded()
}
if (newVal.current >= newVal.max) {
this.left_state.complete()
}
}
}
},
methods: {
resetSearch: function () {
this.search_right = ''
this.search_left = ''
if (this.search_right == '') {
this.right_page = 0
this.right += 1
this.$emit('reset', {'column':'right'})
} else {
this.search_right = ''
}
if (this.search_left == '') {
this.left_page = 0
this.left += 1
this.$emit('reset', {'column':'left'})
} else {
this.search_left = ''
}
},
infiniteHandler: function($state, col) {
let params = {
@@ -155,16 +195,9 @@ export default {
'page': (col==='left') ? this.left_page + 1 : this.right_page + 1,
'column': col
}
// TODO: change this to be an emit and watch a prop to determine if loaded or complete
new Promise((callback) => this.$emit('get-list', params, callback)).then((result) => {
this[col+'_page'] += 1
$state.loaded();
if (!result) { // callback needs to return true if handler should continue loading more data
$state.complete();
}
}).catch(() => {
$state.complete();
})
this[col+'_state'] = $state
this.$emit('get-list', params)
this[col+'_page'] += 1
},
}
}

View File

@@ -1,212 +0,0 @@
<template>
<div row>
<b-card no-body d-flex flex-column :class="{'border border-primary' : over, 'shake': isError}"
refs="keywordCard"
style="height: 10vh;" :style="{'cursor:grab' : draggable}"
@dragover.prevent
@dragenter.prevent
:draggable="draggable"
@dragstart="handleDragStart($event)"
@dragenter="handleDragEnter($event)"
@dragleave="handleDragLeave($event)"
@drop="handleDragDrop($event)">
<b-row no-gutters style="height:inherit;">
<b-col no-gutters md="3" style="height:inherit;">
<b-card-img-lazy style="object-fit: cover; height: 10vh;" :src="keyword_image" v-bind:alt="$t('Recipe_Image')"></b-card-img-lazy>
</b-col>
<b-col no-gutters md="9" style="height:inherit;">
<b-card-body class="m-0 py-0" style="height:inherit;">
<b-card-text class=" h-100 my-0 d-flex flex-column" style="text-overflow: ellipsis">
<h5 class="m-0 mt-1 text-truncate">{{ keyword.name }}</h5>
<div class= "m-0 text-truncate">{{ keyword.description }}</div>
<div class="mt-auto mb-1 d-flex flex-row justify-content-end">
<div v-if="keyword.numchild !=0" class="mx-2 btn btn-link btn-sm"
style="z-index: 800;" v-on:click="$emit('item-action',{'action':'get-children','source':keyword})">
<div v-if="!keyword.expanded">{{keyword.numchild}} {{$t('Keywords')}}</div>
<div v-else>{{$t('Hide Keywords')}}</div>
</div>
<div class="mx-2 btn btn-link btn-sm" style="z-index: 800;"
v-on:click="$emit('item-action',{'action':'get-recipes','source':keyword})">
<div v-if="!keyword.show_recipes">{{keyword.numrecipe}} {{$t('Recipes')}}</div>
<div v-else>{{$t('Hide Recipes')}}</div>
</div>
</div>
</b-card-text>
</b-card-body>
</b-col>
<div class="card-img-overlay justify-content-right h-25 m-0 p-0 text-right">
<generic-context-menu class="p-0"
:show_merge="true"
:show_move="true"
@item-action="$emit('item-action', {'action': $event, 'source': keyword})">
</generic-context-menu>
</div>
</b-row>
</b-card>
<!-- recursively add child keywords -->
<div class="row" v-if="keyword.expanded">
<div class="col-md-11 offset-md-1">
<keyword-card v-for="child in keyword.children"
:keyword="child"
v-bind:key="child.id"
draggable="true"
@item-action="$emit('item-action', $event)">
</keyword-card>
</div>
</div>
<!-- conditionally view recipes -->
<div class="row" v-if="keyword.show_recipes">
<div class="col-md-11 offset-md-1">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));grid-gap: 1rem;">
<recipe-card v-for="r in keyword.recipes"
v-bind:key="r.id"
:recipe="r">
</recipe-card>
</div>
</div>
</div>
<!-- this should be made a generic component, would also require mixin for functions that generate the popup and put in parent container-->
<b-list-group ref="tooltip" variant="light" v-show="show_menu" v-on-clickaway="closeMenu" style="z-index:999; cursor:pointer">
<b-list-group-item action v-on:click="$emit('item-action',{'action': 'move', 'target': keyword, 'source': source}); closeMenu()">
<i class="fas fa-expand-arrows-alt fa-fw"></i> {{$t('Move')}}: {{$t('move_confirmation', {'child': source.name,'parent':keyword.name})}}
</b-list-group-item>
<b-list-group-item action v-on:click="$emit('item-action',{'action': 'merge', 'target': keyword, 'source': source}); closeMenu()">
<i class="fas fa-compress-arrows-alt fa-fw"></i> {{$t('Merge')}}: {{ $t('merge_confirmation', {'source': source.name,'target':keyword.name}) }}
</b-list-group-item>
<b-list-group-item action v-on:click="closeMenu()">
<i class="fas fa-times fa-fw"></i> {{$t('Cancel')}}
</b-list-group-item>
</b-list-group>
</div>
</template>
<script>
import GenericContextMenu from "@/components/GenericContextMenu";
import RecipeCard from "@/components/RecipeCard";
import { mixin as clickaway } from 'vue-clickaway';
import { createPopper } from '@popperjs/core';
export default {
name: "KeywordCard",
components: { GenericContextMenu, RecipeCard },
mixins: [clickaway],
props: {
keyword: Object,
draggable: {type: Boolean, default: false}
},
data() {
return {
keyword_image: '',
over: false,
show_menu: false,
dragMenu: undefined,
isError: false,
source: {},
target: {}
}
},
mounted() {
if (this.keyword == null || this.keyword.image == null) {
this.keyword_image = window.IMAGE_PLACEHOLDER
} else {
this.keyword_image = this.keyword.image
}
this.dragMenu = this.$refs.tooltip
},
methods: {
handleDragStart: function(e) {
this.isError = false
e.dataTransfer.setData('source', JSON.stringify(this.keyword))
},
handleDragEnter: function(e) {
if (!e.currentTarget.contains(e.relatedTarget) && e.relatedTarget != null) {
this.over = true
}
},
handleDragLeave: function(e) {
if (!e.currentTarget.contains(e.relatedTarget)) {
this.over = false
}
},
handleDragDrop: function(e) {
let source = JSON.parse(e.dataTransfer.getData('source'))
if (source.id != this.keyword.id){
this.source = source
let menuLocation = {getBoundingClientRect: this.generateLocation(e.clientX, e.clientY),}
this.show_menu = true
let popper = createPopper(
menuLocation,
this.dragMenu,
{
placement: 'bottom-start',
modifiers: [
{
name: 'preventOverflow',
options: {
rootBoundary: 'document',
},
},
{
name: 'flip',
options: {
fallbackPlacements: ['bottom-end', 'top-start', 'top-end', 'left-start', 'right-start'],
rootBoundary: 'document',
},
},
],
})
popper.update()
this.over = false
this.$emit({'action': 'drop', 'target': this.keyword, 'source': this.source})
} else {
this.isError = true
}
},
generateLocation: function (x = 0, y = 0) {
return () => ({
width: 0,
height: 0,
top: y,
right: x,
bottom: y,
left: x,
});
},
closeMenu: function(){
this.show_menu = false
}
}
}
</script>
<style scoped>
.shake {
animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
transform: translate3d(0, 0, 0);
backface-visibility: hidden;
perspective: 1000px;
}
@keyframes shake {
10%,
90% {
transform: translate3d(-1px, 0, 0);
}
20%,
80% {
transform: translate3d(2px, 0, 0);
}
30%,
50%,
70% {
transform: translate3d(-4px, 0, 0);
}
40%,
60% {
transform: translate3d(4px, 0, 0);
}
}
</style>

View File

@@ -0,0 +1,71 @@
<template>
<div>
<b-form-group
v-bind:label="label"
class="mb-3">
<twemoji-textarea
:ref="'_edit_' + id"
:initialContent="value"
:emojiData="emojiDataAll"
:emojiGroups="emojiGroups"
triggerType="hover"
recentEmojisFeat="true"
recentEmojisStorage="local"
@contentChanged="setIcon"
/>
</b-form-group>
</div>
</template>
<script>
import {TwemojiTextarea} from '@kevinfaguiar/vue-twemoji-picker';
// TODO add localization
import EmojiAllData from '@kevinfaguiar/vue-twemoji-picker/emoji-data/en/emoji-all-groups.json';
import EmojiGroups from '@kevinfaguiar/vue-twemoji-picker/emoji-data/emoji-groups.json';
export default {
name: 'EmojiInput',
components: {TwemojiTextarea},
props: {
field: {type: String, default: 'You Forgot To Set Field Name'},
label: {type: String, default: ''},
value: {type: String, default: ''},
},
data() {
return {
new_value: undefined,
id: null
}
},
computed: {
// modelName() {
// return this?.model?.name ?? this.$t('Search')
// },
emojiDataAll() {
return EmojiAllData;
},
emojiGroups() {
return EmojiGroups;
}
},
watch: {
'new_value': function () {
this.$root.$emit('change', this.field, this.new_value ?? null)
},
},
mounted() {
this.id = this._uid
},
methods: {
prepareEmoji: function() {
this.$refs['_edit_' + this.id].addText(this.this_item.icon || '');
this.$refs['_edit_' + this.id].blur()
document.getElementById('btn-emoji-default').disabled = true;
},
setIcon: function(icon) {
this.new_value = icon
},
}
}
</script>

View File

@@ -12,7 +12,6 @@
:model="listModel(f.list)"
:sticky_options="f.sticky_options || undefined"
@change="storeValue"/> <!-- TODO add ability to create new items associated with lookup -->
<!-- TODO: add emoji field -->
<!-- TODO: add multi-selection input list -->
<checkbox-input v-if="f.type=='checkbox'"
:label="f.label"
@@ -23,6 +22,11 @@
:value="f.value"
:field="f.field"
:placeholder="f.placeholder"/>
<emoji-input v-if="f.type=='emoji'"
:label="f.label"
:value="f.value"
:field="f.field"
@change="storeValue"/>
</div>
<template v-slot:modal-footer>
@@ -43,10 +47,11 @@ import {Models} from "@/utils/models";
import CheckboxInput from "@/components/Modals/CheckboxInput";
import LookupInput from "@/components/Modals/LookupInput";
import TextInput from "@/components/Modals/TextInput";
import EmojiInput from "@/components/Modals/EmojiInput";
export default {
name: 'GenericModalForm',
components: {CheckboxInput, LookupInput, TextInput},
components: {CheckboxInput, LookupInput, TextInput, EmojiInput},
props: {
model: {required: true, type: Object, default: function() {}},
action: {required: true, type: Object, default: function() {}},

View File

@@ -47,10 +47,5 @@ export default {
this.$root.$emit('change', this.field, this.new_value?.id ?? null)
},
},
methods: {
Button: function(e) {
this.$bvModal.show('modal')
}
}
}
</script>

View File

@@ -94,6 +94,7 @@ export default {
}
},
methods: {
// TODO: convert this to genericAPI
clickUrl: function () {
if (this.recipe !== null) {
return resolveDjangoUrl('view_recipe', this.recipe.id)

View File

@@ -121,5 +121,9 @@
"Name": "Name",
"Description": "Description",
"Recipe": "Recipe",
"tree_root": "Root of Tree"
"tree_root": "Root of Tree",
"Icon": "Icon",
"Unit": "Unit",
"No_Results": "No Results",
"New_Unit": "New Unit"
}

View File

@@ -108,10 +108,58 @@ export class Models {
static KEYWORD = {
'name': i18n.t('Keyword'), // *OPTIONAL: parameters will be built model -> model_type -> default
'apiName': 'Keyword',
'model_type': this.TREE
'model_type': this.TREE,
'create': {
// if not defined partialUpdate will use the same parameters, prepending 'id'
'params': [['name', 'description', 'icon']],
'form': {
'name': {
'form_field': true,
'type': 'text',
'field': 'name',
'label': i18n.t('Name'),
'placeholder': ''
},
'description': {
'form_field': true,
'type': 'text',
'field': 'description',
'label': i18n.t('Description'),
'placeholder': ''
},
'icon': {
'form_field': true,
'type': 'emoji',
'field': 'icon',
'label': i18n.t('Icon')
},
}
},
}
static UNIT = {
'name': i18n.t('Unit'),
'apiName': 'Unit',
'create': {
'params': [['name', 'description']],
'form': {
'name': {
'form_field': true,
'type': 'text',
'field': 'name',
'label': i18n.t('Name'),
'placeholder': ''
},
'description': {
'form_field': true,
'type': 'text',
'field': 'description',
'label': i18n.t('Description'),
'placeholder': ''
}
}
},
'move': false
}
static UNIT = {}
static RECIPE = {}
static SHOPPING_LIST = {}
static RECIPE_BOOK = {
'name': i18n.t('Recipe_Book'),
@@ -126,7 +174,7 @@ export class Models {
'name': i18n.t('Recipe'),
'apiName': 'Recipe',
'list': {
'params': ['query', 'keywords', 'foods', 'books', 'keywordsOr', 'foodsOr', 'booksOr', 'internal', 'random', '_new', 'page', 'pageSize', 'options'],
'params': ['query', 'keywords', 'foods', 'units', 'books', 'keywordsOr', 'foodsOr', 'booksOr', 'internal', 'random', '_new', 'page', 'pageSize', 'options'],
'config': {
'foods': {'type':'string'},
'keywords': {'type': 'string'},

View File

@@ -414,10 +414,10 @@ export interface InlineResponse2001 {
previous?: string | null;
/**
*
* @type {Array<Food>}
* @type {Array<Unit>}
* @memberof InlineResponse2001
*/
results?: Array<Food>;
results?: Array<Unit>;
}
/**
*
@@ -445,10 +445,10 @@ export interface InlineResponse2002 {
previous?: string | null;
/**
*
* @type {Array<RecipeOverview>}
* @type {Array<Food>}
* @memberof InlineResponse2002
*/
results?: Array<RecipeOverview>;
results?: Array<Food>;
}
/**
*
@@ -476,10 +476,10 @@ export interface InlineResponse2003 {
previous?: string | null;
/**
*
* @type {Array<ViewLog>}
* @type {Array<RecipeOverview>}
* @memberof InlineResponse2003
*/
results?: Array<ViewLog>;
results?: Array<RecipeOverview>;
}
/**
*
@@ -507,73 +507,11 @@ export interface InlineResponse2004 {
previous?: string | null;
/**
*
* @type {Array<CookLog>}
* @type {Array<SupermarketCategoryRelation>}
* @memberof InlineResponse2004
*/
results?: Array<CookLog>;
}
/**
*
* @export
* @interface InlineResponse2005
*/
export interface InlineResponse2005 {
/**
*
* @type {number}
* @memberof InlineResponse2005
*/
count?: number;
/**
*
* @type {string}
* @memberof InlineResponse2005
*/
next?: string | null;
/**
*
* @type {string}
* @memberof InlineResponse2005
*/
previous?: string | null;
/**
*
* @type {Array<SupermarketCategoryRelation>}
* @memberof InlineResponse2005
*/
results?: Array<SupermarketCategoryRelation>;
}
/**
*
* @export
* @interface InlineResponse2006
*/
export interface InlineResponse2006 {
/**
*
* @type {number}
* @memberof InlineResponse2006
*/
count?: number;
/**
*
* @type {string}
* @memberof InlineResponse2006
*/
next?: string | null;
/**
*
* @type {string}
* @memberof InlineResponse2006
*/
previous?: string | null;
/**
*
* @type {Array<ImportLog>}
* @memberof InlineResponse2006
*/
results?: Array<ImportLog>;
}
/**
*
* @export
@@ -1992,6 +1930,18 @@ export interface StepUnit {
* @memberof StepUnit
*/
description?: string | null;
/**
*
* @type {string}
* @memberof StepUnit
*/
numrecipe?: string;
/**
*
* @type {string}
* @memberof StepUnit
*/
image?: string;
}
/**
*
@@ -2238,6 +2188,18 @@ export interface Unit {
* @memberof Unit
*/
description?: string | null;
/**
*
* @type {string}
* @memberof Unit
*/
numrecipe?: string;
/**
*
* @type {string}
* @memberof Unit
*/
image?: string;
}
/**
*
@@ -4118,12 +4080,10 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
},
/**
*
* @param {number} [page] A page number within the paginated result set.
* @param {number} [pageSize] Number of results to return per page.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listCookLogs: async (page?: number, pageSize?: number, options: any = {}): Promise<RequestArgs> => {
listCookLogs: async (options: any = {}): Promise<RequestArgs> => {
const localVarPath = `/api/cook-log/`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@@ -4136,14 +4096,6 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
if (page !== undefined) {
localVarQueryParameter['page'] = page;
}
if (pageSize !== undefined) {
localVarQueryParameter['page_size'] = pageSize;
}
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
@@ -4211,12 +4163,10 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
},
/**
*
* @param {number} [page] A page number within the paginated result set.
* @param {number} [pageSize] Number of results to return per page.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listImportLogs: async (page?: number, pageSize?: number, options: any = {}): Promise<RequestArgs> => {
listImportLogs: async (options: any = {}): Promise<RequestArgs> => {
const localVarPath = `/api/import-log/`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@@ -4229,14 +4179,6 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
if (page !== undefined) {
localVarQueryParameter['page'] = page;
}
if (pageSize !== undefined) {
localVarQueryParameter['page_size'] = pageSize;
}
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
@@ -4452,6 +4394,7 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
* @param {string} [query] Query string matched (fuzzy) against recipe name. In the future also fulltext search.
* @param {string} [keywords] Id of keyword a recipe should have. For multiple repeat parameter.
* @param {string} [foods] Id of food a recipe should have. For multiple repeat parameter.
* @param {number} [units] Id of unit a recipe should have.
* @param {string} [books] Id of book a recipe should have. For multiple repeat parameter.
* @param {string} [keywordsOr] If recipe should have all (AND) or any (OR) of the provided keywords.
* @param {string} [foodsOr] If recipe should have all (AND) or any (OR) any of the provided foods.
@@ -4464,7 +4407,7 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listRecipes: async (query?: string, keywords?: string, foods?: string, books?: string, keywordsOr?: string, foodsOr?: string, booksOr?: string, internal?: string, random?: string, _new?: string, page?: number, pageSize?: number, options: any = {}): Promise<RequestArgs> => {
listRecipes: async (query?: string, keywords?: string, foods?: string, units?: number, books?: string, keywordsOr?: string, foodsOr?: string, booksOr?: string, internal?: string, random?: string, _new?: string, page?: number, pageSize?: number, options: any = {}): Promise<RequestArgs> => {
const localVarPath = `/api/recipe/`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@@ -4489,6 +4432,10 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
localVarQueryParameter['foods'] = foods;
}
if (units !== undefined) {
localVarQueryParameter['units'] = units;
}
if (books !== undefined) {
localVarQueryParameter['books'] = books;
}
@@ -4838,10 +4785,13 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
},
/**
*
* @param {string} [query] Query string matched against unit name.
* @param {number} [page] A page number within the paginated result set.
* @param {number} [pageSize] Number of results to return per page.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listUnits: async (options: any = {}): Promise<RequestArgs> => {
listUnits: async (query?: string, page?: number, pageSize?: number, options: any = {}): Promise<RequestArgs> => {
const localVarPath = `/api/unit/`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@@ -4854,6 +4804,18 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
if (query !== undefined) {
localVarQueryParameter['query'] = query;
}
if (page !== undefined) {
localVarQueryParameter['page'] = page;
}
if (pageSize !== undefined) {
localVarQueryParameter['page_size'] = pageSize;
}
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
@@ -4954,12 +4916,10 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
},
/**
*
* @param {number} [page] A page number within the paginated result set.
* @param {number} [pageSize] Number of results to return per page.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listViewLogs: async (page?: number, pageSize?: number, options: any = {}): Promise<RequestArgs> => {
listViewLogs: async (options: any = {}): Promise<RequestArgs> => {
const localVarPath = `/api/view-log/`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@@ -4972,14 +4932,6 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
if (page !== undefined) {
localVarQueryParameter['page'] = page;
}
if (pageSize !== undefined) {
localVarQueryParameter['page_size'] = pageSize;
}
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
@@ -5073,6 +5025,47 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
options: localVarRequestOptions,
};
},
/**
*
* @param {string} id A unique integer value identifying this unit.
* @param {string} target
* @param {Unit} [unit]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
mergeUnit: async (id: string, target: string, unit?: Unit, options: any = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('mergeUnit', 'id', id)
// verify required parameter 'target' is not null or undefined
assertParamExists('mergeUnit', 'target', target)
const localVarPath = `/api/unit/{id}/merge/{target}/`
.replace(`{${"id"}}`, encodeURIComponent(String(id)))
.replace(`{${"target"}}`, encodeURIComponent(String(target)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(unit, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {string} id A unique integer value identifying this food.
@@ -8348,13 +8341,11 @@ export const ApiApiFp = function(configuration?: Configuration) {
},
/**
*
* @param {number} [page] A page number within the paginated result set.
* @param {number} [pageSize] Number of results to return per page.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async listCookLogs(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2004>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listCookLogs(page, pageSize, options);
async listCookLogs(options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<CookLog>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listCookLogs(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
@@ -8367,19 +8358,17 @@ export const ApiApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async listFoods(query?: string, root?: number, tree?: number, page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2001>> {
async listFoods(query?: string, root?: number, tree?: number, page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2002>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listFoods(query, root, tree, page, pageSize, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {number} [page] A page number within the paginated result set.
* @param {number} [pageSize] Number of results to return per page.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async listImportLogs(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2006>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listImportLogs(page, pageSize, options);
async listImportLogs(options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<ImportLog>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listImportLogs(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
@@ -8446,6 +8435,7 @@ export const ApiApiFp = function(configuration?: Configuration) {
* @param {string} [query] Query string matched (fuzzy) against recipe name. In the future also fulltext search.
* @param {string} [keywords] Id of keyword a recipe should have. For multiple repeat parameter.
* @param {string} [foods] Id of food a recipe should have. For multiple repeat parameter.
* @param {number} [units] Id of unit a recipe should have.
* @param {string} [books] Id of book a recipe should have. For multiple repeat parameter.
* @param {string} [keywordsOr] If recipe should have all (AND) or any (OR) of the provided keywords.
* @param {string} [foodsOr] If recipe should have all (AND) or any (OR) any of the provided foods.
@@ -8458,8 +8448,8 @@ export const ApiApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async listRecipes(query?: string, keywords?: string, foods?: string, books?: string, keywordsOr?: string, foodsOr?: string, booksOr?: string, internal?: string, random?: string, _new?: string, page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2002>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listRecipes(query, keywords, foods, books, keywordsOr, foodsOr, booksOr, internal, random, _new, page, pageSize, options);
async listRecipes(query?: string, keywords?: string, foods?: string, units?: number, books?: string, keywordsOr?: string, foodsOr?: string, booksOr?: string, internal?: string, random?: string, _new?: string, page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2003>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listRecipes(query, keywords, foods, units, books, keywordsOr, foodsOr, booksOr, internal, random, _new, page, pageSize, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
@@ -8514,7 +8504,7 @@ export const ApiApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async listSupermarketCategoryRelations(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2005>> {
async listSupermarketCategoryRelations(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2004>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listSupermarketCategoryRelations(page, pageSize, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
@@ -8556,11 +8546,14 @@ export const ApiApiFp = function(configuration?: Configuration) {
},
/**
*
* @param {string} [query] Query string matched against unit name.
* @param {number} [page] A page number within the paginated result set.
* @param {number} [pageSize] Number of results to return per page.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async listUnits(options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<Unit>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listUnits(options);
async listUnits(query?: string, page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2001>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listUnits(query, page, pageSize, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
@@ -8592,13 +8585,11 @@ export const ApiApiFp = function(configuration?: Configuration) {
},
/**
*
* @param {number} [page] A page number within the paginated result set.
* @param {number} [pageSize] Number of results to return per page.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async listViewLogs(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2003>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listViewLogs(page, pageSize, options);
async listViewLogs(options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<ViewLog>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listViewLogs(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
@@ -8625,6 +8616,18 @@ export const ApiApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.mergeKeyword(id, target, keyword, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id A unique integer value identifying this unit.
* @param {string} target
* @param {Unit} [unit]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async mergeUnit(id: string, target: string, unit?: Unit, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Unit>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.mergeUnit(id, target, unit, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id A unique integer value identifying this food.
@@ -9908,13 +9911,11 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
},
/**
*
* @param {number} [page] A page number within the paginated result set.
* @param {number} [pageSize] Number of results to return per page.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listCookLogs(page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2004> {
return localVarFp.listCookLogs(page, pageSize, options).then((request) => request(axios, basePath));
listCookLogs(options?: any): AxiosPromise<Array<CookLog>> {
return localVarFp.listCookLogs(options).then((request) => request(axios, basePath));
},
/**
*
@@ -9926,18 +9927,16 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listFoods(query?: string, root?: number, tree?: number, page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2001> {
listFoods(query?: string, root?: number, tree?: number, page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2002> {
return localVarFp.listFoods(query, root, tree, page, pageSize, options).then((request) => request(axios, basePath));
},
/**
*
* @param {number} [page] A page number within the paginated result set.
* @param {number} [pageSize] Number of results to return per page.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listImportLogs(page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2006> {
return localVarFp.listImportLogs(page, pageSize, options).then((request) => request(axios, basePath));
listImportLogs(options?: any): AxiosPromise<Array<ImportLog>> {
return localVarFp.listImportLogs(options).then((request) => request(axios, basePath));
},
/**
*
@@ -9997,6 +9996,7 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
* @param {string} [query] Query string matched (fuzzy) against recipe name. In the future also fulltext search.
* @param {string} [keywords] Id of keyword a recipe should have. For multiple repeat parameter.
* @param {string} [foods] Id of food a recipe should have. For multiple repeat parameter.
* @param {number} [units] Id of unit a recipe should have.
* @param {string} [books] Id of book a recipe should have. For multiple repeat parameter.
* @param {string} [keywordsOr] If recipe should have all (AND) or any (OR) of the provided keywords.
* @param {string} [foodsOr] If recipe should have all (AND) or any (OR) any of the provided foods.
@@ -10009,8 +10009,8 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listRecipes(query?: string, keywords?: string, foods?: string, books?: string, keywordsOr?: string, foodsOr?: string, booksOr?: string, internal?: string, random?: string, _new?: string, page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2002> {
return localVarFp.listRecipes(query, keywords, foods, books, keywordsOr, foodsOr, booksOr, internal, random, _new, page, pageSize, options).then((request) => request(axios, basePath));
listRecipes(query?: string, keywords?: string, foods?: string, units?: number, books?: string, keywordsOr?: string, foodsOr?: string, booksOr?: string, internal?: string, random?: string, _new?: string, page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2003> {
return localVarFp.listRecipes(query, keywords, foods, units, books, keywordsOr, foodsOr, booksOr, internal, random, _new, page, pageSize, options).then((request) => request(axios, basePath));
},
/**
*
@@ -10059,7 +10059,7 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listSupermarketCategoryRelations(page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2005> {
listSupermarketCategoryRelations(page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2004> {
return localVarFp.listSupermarketCategoryRelations(page, pageSize, options).then((request) => request(axios, basePath));
},
/**
@@ -10096,11 +10096,14 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
},
/**
*
* @param {string} [query] Query string matched against unit name.
* @param {number} [page] A page number within the paginated result set.
* @param {number} [pageSize] Number of results to return per page.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listUnits(options?: any): AxiosPromise<Array<Unit>> {
return localVarFp.listUnits(options).then((request) => request(axios, basePath));
listUnits(query?: string, page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2001> {
return localVarFp.listUnits(query, page, pageSize, options).then((request) => request(axios, basePath));
},
/**
*
@@ -10128,13 +10131,11 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
},
/**
*
* @param {number} [page] A page number within the paginated result set.
* @param {number} [pageSize] Number of results to return per page.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listViewLogs(page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2003> {
return localVarFp.listViewLogs(page, pageSize, options).then((request) => request(axios, basePath));
listViewLogs(options?: any): AxiosPromise<Array<ViewLog>> {
return localVarFp.listViewLogs(options).then((request) => request(axios, basePath));
},
/**
*
@@ -10158,6 +10159,17 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
mergeKeyword(id: string, target: string, keyword?: Keyword, options?: any): AxiosPromise<Keyword> {
return localVarFp.mergeKeyword(id, target, keyword, options).then((request) => request(axios, basePath));
},
/**
*
* @param {string} id A unique integer value identifying this unit.
* @param {string} target
* @param {Unit} [unit]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
mergeUnit(id: string, target: string, unit?: Unit, options?: any): AxiosPromise<Unit> {
return localVarFp.mergeUnit(id, target, unit, options).then((request) => request(axios, basePath));
},
/**
*
* @param {string} id A unique integer value identifying this food.
@@ -11465,14 +11477,12 @@ export class ApiApi extends BaseAPI {
/**
*
* @param {number} [page] A page number within the paginated result set.
* @param {number} [pageSize] Number of results to return per page.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ApiApi
*/
public listCookLogs(page?: number, pageSize?: number, options?: any) {
return ApiApiFp(this.configuration).listCookLogs(page, pageSize, options).then((request) => request(this.axios, this.basePath));
public listCookLogs(options?: any) {
return ApiApiFp(this.configuration).listCookLogs(options).then((request) => request(this.axios, this.basePath));
}
/**
@@ -11492,14 +11502,12 @@ export class ApiApi extends BaseAPI {
/**
*
* @param {number} [page] A page number within the paginated result set.
* @param {number} [pageSize] Number of results to return per page.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ApiApi
*/
public listImportLogs(page?: number, pageSize?: number, options?: any) {
return ApiApiFp(this.configuration).listImportLogs(page, pageSize, options).then((request) => request(this.axios, this.basePath));
public listImportLogs(options?: any) {
return ApiApiFp(this.configuration).listImportLogs(options).then((request) => request(this.axios, this.basePath));
}
/**
@@ -11572,6 +11580,7 @@ export class ApiApi extends BaseAPI {
* @param {string} [query] Query string matched (fuzzy) against recipe name. In the future also fulltext search.
* @param {string} [keywords] Id of keyword a recipe should have. For multiple repeat parameter.
* @param {string} [foods] Id of food a recipe should have. For multiple repeat parameter.
* @param {number} [units] Id of unit a recipe should have.
* @param {string} [books] Id of book a recipe should have. For multiple repeat parameter.
* @param {string} [keywordsOr] If recipe should have all (AND) or any (OR) of the provided keywords.
* @param {string} [foodsOr] If recipe should have all (AND) or any (OR) any of the provided foods.
@@ -11585,8 +11594,8 @@ export class ApiApi extends BaseAPI {
* @throws {RequiredError}
* @memberof ApiApi
*/
public listRecipes(query?: string, keywords?: string, foods?: string, books?: string, keywordsOr?: string, foodsOr?: string, booksOr?: string, internal?: string, random?: string, _new?: string, page?: number, pageSize?: number, options?: any) {
return ApiApiFp(this.configuration).listRecipes(query, keywords, foods, books, keywordsOr, foodsOr, booksOr, internal, random, _new, page, pageSize, options).then((request) => request(this.axios, this.basePath));
public listRecipes(query?: string, keywords?: string, foods?: string, units?: number, books?: string, keywordsOr?: string, foodsOr?: string, booksOr?: string, internal?: string, random?: string, _new?: string, page?: number, pageSize?: number, options?: any) {
return ApiApiFp(this.configuration).listRecipes(query, keywords, foods, units, books, keywordsOr, foodsOr, booksOr, internal, random, _new, page, pageSize, options).then((request) => request(this.axios, this.basePath));
}
/**
@@ -11693,12 +11702,15 @@ export class ApiApi extends BaseAPI {
/**
*
* @param {string} [query] Query string matched against unit name.
* @param {number} [page] A page number within the paginated result set.
* @param {number} [pageSize] Number of results to return per page.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ApiApi
*/
public listUnits(options?: any) {
return ApiApiFp(this.configuration).listUnits(options).then((request) => request(this.axios, this.basePath));
public listUnits(query?: string, page?: number, pageSize?: number, options?: any) {
return ApiApiFp(this.configuration).listUnits(query, page, pageSize, options).then((request) => request(this.axios, this.basePath));
}
/**
@@ -11733,14 +11745,12 @@ export class ApiApi extends BaseAPI {
/**
*
* @param {number} [page] A page number within the paginated result set.
* @param {number} [pageSize] Number of results to return per page.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ApiApi
*/
public listViewLogs(page?: number, pageSize?: number, options?: any) {
return ApiApiFp(this.configuration).listViewLogs(page, pageSize, options).then((request) => request(this.axios, this.basePath));
public listViewLogs(options?: any) {
return ApiApiFp(this.configuration).listViewLogs(options).then((request) => request(this.axios, this.basePath));
}
/**
@@ -11769,6 +11779,19 @@ export class ApiApi extends BaseAPI {
return ApiApiFp(this.configuration).mergeKeyword(id, target, keyword, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {string} id A unique integer value identifying this unit.
* @param {string} target
* @param {Unit} [unit]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ApiApi
*/
public mergeUnit(id: string, target: string, unit?: Unit, options?: any) {
return ApiApiFp(this.configuration).mergeUnit(id, target, unit, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {string} id A unique integer value identifying this food.

View File

@@ -25,12 +25,8 @@ const pages = {
entry: './src/apps/UserFileView/main.js',
chunks: ['chunk-vendors']
},
'keyword_list_view': {
entry: './src/apps/KeywordListView/main.js',
chunks: ['chunk-vendors']
},
'food_list_view': {
entry: './src/apps/FoodListView/main.js',
'model_list_view': {
entry: './src/apps/ModelListView/main.js',
chunks: ['chunk-vendors']
},
}