mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-03 05:11:31 -05:00
Merge branch 'develop' of https://github.com/vabene1111/recipes into develop
# Conflicts: # vue/src/components/Modals/LookupInput.vue
This commit is contained in:
@@ -45,7 +45,8 @@ SHOPPING_MIN_AUTOSYNC_INTERVAL=5
|
|||||||
# Default for user setting sticky navbar
|
# Default for user setting sticky navbar
|
||||||
# STICKY_NAV_PREF_DEFAULT=1
|
# STICKY_NAV_PREF_DEFAULT=1
|
||||||
|
|
||||||
# If base URL is something other than just / (you are serving a subfolder in your proxy for instance http://recipe_app/recipes/)
|
# If base URL is something other than just / (you are serving a subfolder in your proxy for instance http://recipe_app/recipes/)
|
||||||
|
# Be sure to not have a trailing slash: e.g. '/recipes' instead of '/recipes/'
|
||||||
# SCRIPT_NAME=/recipes
|
# SCRIPT_NAME=/recipes
|
||||||
|
|
||||||
# If staticfiles are stored at a different location uncomment and change accordingly, MUST END IN /
|
# If staticfiles are stored at a different location uncomment and change accordingly, MUST END IN /
|
||||||
@@ -148,4 +149,4 @@ REVERSE_PROXY_AUTH=0
|
|||||||
|
|
||||||
# Enables exporting PDF (see export docs)
|
# Enables exporting PDF (see export docs)
|
||||||
# Disabled by default, uncomment to enable
|
# Disabled by default, uncomment to enable
|
||||||
# ENABLE_PDF_EXPORT=1
|
# ENABLE_PDF_EXPORT=1
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from .models import (BookmarkletImport, Comment, CookLog, Food, FoodInheritField
|
|||||||
Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchPreference, ShareLink,
|
Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchPreference, ShareLink,
|
||||||
ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
|
ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
|
||||||
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog,
|
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog,
|
||||||
TelegramBot, Unit, UserFile, UserPreference, ViewLog)
|
TelegramBot, Unit, UserFile, UserPreference, ViewLog, Automation)
|
||||||
|
|
||||||
|
|
||||||
class CustomUserAdmin(UserAdmin):
|
class CustomUserAdmin(UserAdmin):
|
||||||
@@ -29,11 +29,52 @@ admin.site.register(User, CustomUserAdmin)
|
|||||||
admin.site.unregister(Group)
|
admin.site.unregister(Group)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.action(description='Delete all data from a space')
|
||||||
|
def delete_space_action(modeladmin, request, queryset):
|
||||||
|
for space in queryset:
|
||||||
|
CookLog.objects.filter(space=space).delete()
|
||||||
|
ViewLog.objects.filter(space=space).delete()
|
||||||
|
ImportLog.objects.filter(space=space).delete()
|
||||||
|
BookmarkletImport.objects.filter(space=space).delete()
|
||||||
|
|
||||||
|
Comment.objects.filter(recipe__space=space).delete()
|
||||||
|
Keyword.objects.filter(space=space).delete()
|
||||||
|
Ingredient.objects.filter(space=space).delete()
|
||||||
|
Food.objects.filter(space=space).delete()
|
||||||
|
Unit.objects.filter(space=space).delete()
|
||||||
|
Step.objects.filter(space=space).delete()
|
||||||
|
NutritionInformation.objects.filter(space=space).delete()
|
||||||
|
RecipeBookEntry.objects.filter(book__space=space).delete()
|
||||||
|
RecipeBook.objects.filter(space=space).delete()
|
||||||
|
MealType.objects.filter(space=space).delete()
|
||||||
|
MealPlan.objects.filter(space=space).delete()
|
||||||
|
ShareLink.objects.filter(space=space).delete()
|
||||||
|
Recipe.objects.filter(space=space).delete()
|
||||||
|
|
||||||
|
RecipeImport.objects.filter(space=space).delete()
|
||||||
|
SyncLog.objects.filter(sync__space=space).delete()
|
||||||
|
Sync.objects.filter(space=space).delete()
|
||||||
|
Storage.objects.filter(space=space).delete()
|
||||||
|
|
||||||
|
ShoppingListEntry.objects.filter(shoppinglist__space=space).delete()
|
||||||
|
ShoppingListRecipe.objects.filter(shoppinglist__space=space).delete()
|
||||||
|
ShoppingList.objects.filter(space=space).delete()
|
||||||
|
|
||||||
|
SupermarketCategoryRelation.objects.filter(supermarket__space=space).delete()
|
||||||
|
SupermarketCategory.objects.filter(space=space).delete()
|
||||||
|
Supermarket.objects.filter(space=space).delete()
|
||||||
|
|
||||||
|
InviteLink.objects.filter(space=space).delete()
|
||||||
|
UserFile.objects.filter(space=space).delete()
|
||||||
|
Automation.objects.filter(space=space).delete()
|
||||||
|
|
||||||
|
|
||||||
class SpaceAdmin(admin.ModelAdmin):
|
class SpaceAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name', 'created_by', 'max_recipes', 'max_users', 'max_file_storage_mb', 'allow_sharing')
|
list_display = ('name', 'created_by', 'max_recipes', 'max_users', 'max_file_storage_mb', 'allow_sharing')
|
||||||
search_fields = ('name', 'created_by__username')
|
search_fields = ('name', 'created_by__username')
|
||||||
list_filter = ('max_recipes', 'max_users', 'max_file_storage_mb', 'allow_sharing')
|
list_filter = ('max_recipes', 'max_users', 'max_file_storage_mb', 'allow_sharing')
|
||||||
date_hierarchy = 'created_at'
|
date_hierarchy = 'created_at'
|
||||||
|
actions = [delete_space_action]
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Space, SpaceAdmin)
|
admin.site.register(Space, SpaceAdmin)
|
||||||
@@ -128,7 +169,7 @@ def sort_tree(modeladmin, request, queryset):
|
|||||||
class KeywordAdmin(TreeAdmin):
|
class KeywordAdmin(TreeAdmin):
|
||||||
form = movenodeform_factory(Keyword)
|
form = movenodeform_factory(Keyword)
|
||||||
ordering = ('space', 'path',)
|
ordering = ('space', 'path',)
|
||||||
search_fields = ('name', )
|
search_fields = ('name',)
|
||||||
actions = [sort_tree, enable_tree_sorting, disable_tree_sorting]
|
actions = [sort_tree, enable_tree_sorting, disable_tree_sorting]
|
||||||
|
|
||||||
|
|
||||||
@@ -171,13 +212,15 @@ class RecipeAdmin(admin.ModelAdmin):
|
|||||||
admin.site.register(Recipe, RecipeAdmin)
|
admin.site.register(Recipe, RecipeAdmin)
|
||||||
|
|
||||||
admin.site.register(Unit)
|
admin.site.register(Unit)
|
||||||
|
|
||||||
|
|
||||||
# admin.site.register(FoodInheritField)
|
# admin.site.register(FoodInheritField)
|
||||||
|
|
||||||
|
|
||||||
class FoodAdmin(TreeAdmin):
|
class FoodAdmin(TreeAdmin):
|
||||||
form = movenodeform_factory(Keyword)
|
form = movenodeform_factory(Keyword)
|
||||||
ordering = ('space', 'path',)
|
ordering = ('space', 'path',)
|
||||||
search_fields = ('name', )
|
search_fields = ('name',)
|
||||||
actions = [sort_tree, enable_tree_sorting, disable_tree_sorting]
|
actions = [sort_tree, enable_tree_sorting, disable_tree_sorting]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -172,6 +172,8 @@ class RecipeSearch():
|
|||||||
def keyword_filters(self, keywords=None, operator=True):
|
def keyword_filters(self, keywords=None, operator=True):
|
||||||
if not keywords:
|
if not keywords:
|
||||||
return
|
return
|
||||||
|
if not isinstance(keywords, list):
|
||||||
|
keywords = [keywords]
|
||||||
if operator == True:
|
if operator == True:
|
||||||
# TODO creating setting to include descendants of keywords a setting
|
# TODO creating setting to include descendants of keywords a setting
|
||||||
self._queryset = self._queryset.filter(keywords__in=Keyword.include_descendants(Keyword.objects.filter(pk__in=keywords)))
|
self._queryset = self._queryset.filter(keywords__in=Keyword.include_descendants(Keyword.objects.filter(pk__in=keywords)))
|
||||||
@@ -184,6 +186,8 @@ class RecipeSearch():
|
|||||||
def food_filters(self, foods=None, operator=True):
|
def food_filters(self, foods=None, operator=True):
|
||||||
if not foods:
|
if not foods:
|
||||||
return
|
return
|
||||||
|
if not isinstance(foods, list):
|
||||||
|
foods = [foods]
|
||||||
if operator == True:
|
if operator == True:
|
||||||
# TODO creating setting to include descendants of food a setting
|
# TODO creating setting to include descendants of food a setting
|
||||||
self._queryset = self._queryset.filter(steps__ingredients__food__in=Food.include_descendants(Food.objects.filter(pk__in=foods)))
|
self._queryset = self._queryset.filter(steps__ingredients__food__in=Food.include_descendants(Food.objects.filter(pk__in=foods)))
|
||||||
@@ -198,7 +202,9 @@ class RecipeSearch():
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
if not units:
|
if not units:
|
||||||
return
|
return
|
||||||
self._queryset = self._queryset.filter(steps__ingredients__unit__id=units)
|
if not isinstance(units, list):
|
||||||
|
units = [units]
|
||||||
|
self._queryset = self._queryset.filter(steps__ingredients__unit__in=units)
|
||||||
|
|
||||||
def rating_filter(self, rating=None):
|
def rating_filter(self, rating=None):
|
||||||
if rating is None:
|
if rating is None:
|
||||||
@@ -217,6 +223,8 @@ class RecipeSearch():
|
|||||||
def book_filters(self, books=None, operator=True):
|
def book_filters(self, books=None, operator=True):
|
||||||
if not books:
|
if not books:
|
||||||
return
|
return
|
||||||
|
if not isinstance(books, list):
|
||||||
|
books = [books]
|
||||||
if operator == True:
|
if operator == True:
|
||||||
self._queryset = self._queryset.filter(recipebookentry__book__id__in=books)
|
self._queryset = self._queryset.filter(recipebookentry__book__id__in=books)
|
||||||
else:
|
else:
|
||||||
@@ -228,6 +236,8 @@ class RecipeSearch():
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
if not steps:
|
if not steps:
|
||||||
return
|
return
|
||||||
|
if not isinstance(steps, list):
|
||||||
|
steps = [unistepsts]
|
||||||
self._queryset = self._queryset.filter(steps__id__in=steps)
|
self._queryset = self._queryset.filter(steps__id__in=steps)
|
||||||
|
|
||||||
def build_fulltext_filters(self, string=None):
|
def build_fulltext_filters(self, string=None):
|
||||||
@@ -490,7 +500,7 @@ class RecipeFacet():
|
|||||||
'space': self._request.space,
|
'space': self._request.space,
|
||||||
}
|
}
|
||||||
elif self.hash_key is not None:
|
elif self.hash_key is not None:
|
||||||
self._recipe_list = self._cache.get('recipe_list', None)
|
self._recipe_list = self._cache.get('recipe_list', [])
|
||||||
self._search_params = {
|
self._search_params = {
|
||||||
'keyword_list': self._cache.get('keyword_list', None),
|
'keyword_list': self._cache.get('keyword_list', None),
|
||||||
'food_list': self._cache.get('food_list', None),
|
'food_list': self._cache.get('food_list', None),
|
||||||
@@ -637,7 +647,7 @@ class RecipeFacet():
|
|||||||
depth = getattr(keyword, 'depth', 0) + 1
|
depth = getattr(keyword, 'depth', 0) + 1
|
||||||
steplen = depth * Keyword.steplen
|
steplen = depth * Keyword.steplen
|
||||||
|
|
||||||
return queryset.annotate(count=Coalesce(Subquery(self._recipe_count_queryset('keywords', depth, steplen)), 0)
|
return queryset.annotate(count=Coalesce(1, 0)
|
||||||
).filter(depth=depth, count__gt=0
|
).filter(depth=depth, count__gt=0
|
||||||
).values('id', 'name', 'count', 'numchild').order_by('name')
|
).values('id', 'name', 'count', 'numchild').order_by('name')
|
||||||
|
|
||||||
@@ -645,7 +655,7 @@ class RecipeFacet():
|
|||||||
depth = getattr(food, 'depth', 0) + 1
|
depth = getattr(food, 'depth', 0) + 1
|
||||||
steplen = depth * Food.steplen
|
steplen = depth * Food.steplen
|
||||||
|
|
||||||
return queryset.annotate(count=Coalesce(Subquery(self._recipe_count_queryset('steps__ingredients__food', depth, steplen)), 0)
|
return queryset.annotate(count=Coalesce(1, 0)
|
||||||
).filter(depth__lte=depth, count__gt=0
|
).filter(depth__lte=depth, count__gt=0
|
||||||
).values('id', 'name', 'count', 'numchild').order_by('name')
|
).values('id', 'name', 'count', 'numchild').order_by('name')
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import json
|
|||||||
from io import BytesIO, StringIO
|
from io import BytesIO, StringIO
|
||||||
from re import match
|
from re import match
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
from django.utils.text import get_valid_filename
|
|
||||||
|
|
||||||
from rest_framework.renderers import JSONRenderer
|
from rest_framework.renderers import JSONRenderer
|
||||||
|
|
||||||
@@ -57,8 +56,7 @@ class Default(Integration):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
recipe_zip_obj.close()
|
recipe_zip_obj.close()
|
||||||
|
export_zip_obj.writestr(str(r.pk) + '.zip', recipe_zip_stream.getvalue())
|
||||||
export_zip_obj.writestr(get_valid_filename(r.name) + '.zip', recipe_zip_stream.getvalue())
|
|
||||||
export_zip_obj.close()
|
export_zip_obj.close()
|
||||||
|
|
||||||
return [[ 'export.zip', export_zip_stream.getvalue() ]]
|
return [[ 'export.zip', export_zip_stream.getvalue() ]]
|
||||||
@@ -42,7 +42,7 @@ class Integration:
|
|||||||
try:
|
try:
|
||||||
last_kw = Keyword.objects.filter(name__regex=r'^(Import [0-9]+)', space=request.space).latest('created_at')
|
last_kw = Keyword.objects.filter(name__regex=r'^(Import [0-9]+)', space=request.space).latest('created_at')
|
||||||
name = f'Import {int(last_kw.name.replace("Import ", "")) + 1}'
|
name = f'Import {int(last_kw.name.replace("Import ", "")) + 1}'
|
||||||
except ObjectDoesNotExist:
|
except (ObjectDoesNotExist, ValueError):
|
||||||
name = 'Import 1'
|
name = 'Import 1'
|
||||||
|
|
||||||
parent, created = Keyword.objects.get_or_create(name='Import', space=request.space)
|
parent, created = Keyword.objects.get_or_create(name='Import', space=request.space)
|
||||||
@@ -53,7 +53,7 @@ class Integration:
|
|||||||
icon=icon,
|
icon=icon,
|
||||||
space=request.space
|
space=request.space
|
||||||
)
|
)
|
||||||
except IntegrityError: # in case, for whatever reason, the name does exist append UUID to it. Not nice but works for now.
|
except (IntegrityError, ValueError): # in case, for whatever reason, the name does exist append UUID to it. Not nice but works for now.
|
||||||
self.keyword = parent.add_child(
|
self.keyword = parent.add_child(
|
||||||
name=f'{name} {str(uuid.uuid4())[0:8]}',
|
name=f'{name} {str(uuid.uuid4())[0:8]}',
|
||||||
description=description,
|
description=description,
|
||||||
@@ -86,12 +86,10 @@ class Integration:
|
|||||||
export_obj.close()
|
export_obj.close()
|
||||||
export_file = export_stream.getvalue()
|
export_file = export_stream.getvalue()
|
||||||
|
|
||||||
|
|
||||||
response = HttpResponse(export_file, content_type='application/force-download')
|
response = HttpResponse(export_file, content_type='application/force-download')
|
||||||
response['Content-Disposition'] = 'attachment; filename="'+export_filename+'"'
|
response['Content-Disposition'] = 'attachment; filename="' + export_filename + '"'
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
def import_file_name_filter(self, zip_info_object):
|
def import_file_name_filter(self, zip_info_object):
|
||||||
"""
|
"""
|
||||||
Since zipfile.namelist() returns all files in all subdirectories this function allows filtering of files
|
Since zipfile.namelist() returns all files in all subdirectories this function allows filtering of files
|
||||||
@@ -262,7 +260,6 @@ class Integration:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError('Method not implemented in integration')
|
raise NotImplementedError('Method not implemented in integration')
|
||||||
|
|
||||||
|
|
||||||
def get_files_from_recipes(self, recipes, cookie):
|
def get_files_from_recipes(self, recipes, cookie):
|
||||||
"""
|
"""
|
||||||
Takes a list of recipe object and converts it to a array containing each file.
|
Takes a list of recipe object and converts it to a array containing each file.
|
||||||
|
|||||||
@@ -29,7 +29,11 @@ class Nextcloud(Provider):
|
|||||||
client = Nextcloud.get_client(monitor.storage)
|
client = Nextcloud.get_client(monitor.storage)
|
||||||
|
|
||||||
files = client.list(monitor.path)
|
files = client.list(monitor.path)
|
||||||
files.pop(0) # remove first element because its the folder itself
|
|
||||||
|
try:
|
||||||
|
files.pop(0) # remove first element because its the folder itself
|
||||||
|
except IndexError:
|
||||||
|
pass # folder is emtpy, no recipes will be imported
|
||||||
|
|
||||||
import_count = 0
|
import_count = 0
|
||||||
for file in files:
|
for file in files:
|
||||||
|
|||||||
@@ -165,9 +165,10 @@ class FoodInheritFieldSerializer(WritableNestedModelSerializer):
|
|||||||
read_only_fields = ['id']
|
read_only_fields = ['id']
|
||||||
|
|
||||||
|
|
||||||
class UserPreferenceSerializer(serializers.ModelSerializer):
|
class UserPreferenceSerializer(WritableNestedModelSerializer):
|
||||||
food_inherit_default = FoodInheritFieldSerializer(source='space.food_inherit', many=True, allow_null=True, required=False, read_only=True)
|
food_inherit_default = FoodInheritFieldSerializer(source='space.food_inherit', many=True, allow_null=True, required=False, read_only=True)
|
||||||
plan_share = UserNameSerializer(many=True, allow_null=True, required=False, read_only=True)
|
plan_share = UserNameSerializer(many=True, allow_null=True, required=False, read_only=True)
|
||||||
|
shopping_share = UserNameSerializer(many=True, allow_null=True, required=False)
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
if not validated_data.get('user', None):
|
if not validated_data.get('user', None):
|
||||||
@@ -475,7 +476,7 @@ class StepSerializer(WritableNestedModelSerializer, ExtendedRecipeMixin):
|
|||||||
# check if root type is recipe to prevent infinite recursion
|
# check if root type is recipe to prevent infinite recursion
|
||||||
# can be improved later to allow multi level embedding
|
# can be improved later to allow multi level embedding
|
||||||
if obj.step_recipe and type(self.parent.root) == RecipeSerializer:
|
if obj.step_recipe and type(self.parent.root) == RecipeSerializer:
|
||||||
return StepRecipeSerializer(obj.step_recipe).data
|
return StepRecipeSerializer(obj.step_recipe, context={'request': self.context['request']}).data
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Step
|
model = Step
|
||||||
@@ -496,6 +497,11 @@ class StepRecipeSerializer(WritableNestedModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class NutritionInformationSerializer(serializers.ModelSerializer):
|
class NutritionInformationSerializer(serializers.ModelSerializer):
|
||||||
|
carbohydrates = CustomDecimalField()
|
||||||
|
fats = CustomDecimalField()
|
||||||
|
proteins = CustomDecimalField()
|
||||||
|
calories = CustomDecimalField()
|
||||||
|
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
validated_data['space'] = self.context['request'].space
|
validated_data['space'] = self.context['request'].space
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12 col-lg-6 col-md-6 offset-lg-3 offset-md-3">
|
<div class="col-sm-12 col-lg-6 col-md-6 offset-lg-3 offset-md-3">
|
||||||
<hr>
|
<hr>
|
||||||
<form class="login" method="POST" action="{% url 'account_login' %}">
|
<form class="login" method="POST" action="{% url 'account_login' %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form | crispy }}
|
{{ form | crispy }}
|
||||||
@@ -29,12 +29,16 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<button class="btn btn-success" type="submit">{% trans "Sign In" %}</button>
|
<button class="btn btn-success" type="submit">{% trans "Sign In" %}</button>
|
||||||
<a class="btn btn-secondary" href="{% url 'account_signup' %}">{% trans "Sign Up" %}</a>
|
|
||||||
|
{% if SIGNUP_ENABLED %}
|
||||||
|
<a class="btn btn-secondary" href="{% url 'account_signup' %}">{% trans "Sign Up" %}</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if EMAIL_ENABLED %}
|
{% if EMAIL_ENABLED %}
|
||||||
<a class="btn btn-warning float-right d-none d-xl-block d-lg-block"
|
<a class="btn btn-warning float-right d-none d-xl-block d-lg-block"
|
||||||
href="{% url 'account_reset_password' %}">{% trans "Reset My Password" %}</a>
|
href="{% url 'account_reset_password' %}">{% trans "Reset My Password" %}</a>
|
||||||
<p class="d-xl-none d-lg-none">{% trans 'Lost your password?' %} <a href="{% url 'account_reset_password' %}">{% trans "Reset My Password" %}</a></p>
|
<p class="d-xl-none d-lg-none">{% trans 'Lost your password?' %} <a
|
||||||
|
href="{% url 'account_reset_password' %}">{% trans "Reset My Password" %}</a></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,7 +48,7 @@
|
|||||||
|
|
||||||
{% if socialaccount_providers %}
|
{% if socialaccount_providers %}
|
||||||
<div class="row" style="margin-top: 2vh">
|
<div class="row" style="margin-top: 2vh">
|
||||||
<div class="col-sm-12 col-lg-6 col-md-6 offset-lg-3 offset-md-3">
|
<div class="col-sm-12 col-lg-6 col-md-6 offset-lg-3 offset-md-3">
|
||||||
<h5>{% trans "Social Login" %}</h5>
|
<h5>{% trans "Social Login" %}</h5>
|
||||||
<span>{% trans 'You can use any of the following providers to sign in.' %}</span>
|
<span>{% trans 'You can use any of the following providers to sign in.' %}</span>
|
||||||
|
|
||||||
@@ -62,5 +66,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$('#id_login').focus()
|
||||||
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -71,4 +71,8 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$('#id_username').focus()
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -660,13 +660,14 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="{% url 'javascript-catalog' %}"></script>
|
<script src="{% url 'javascript-catalog' %}">
|
||||||
|
</script>
|
||||||
<script type="application/javascript">
|
<script type="application/javascript">
|
||||||
let csrftoken = Cookies.get('csrftoken');
|
let csrftoken = Cookies.get('csrftoken');
|
||||||
Vue.http.headers.common['X-CSRFToken'] = csrftoken;
|
Vue.http.headers.common['X-CSRFToken'] = csrftoken;
|
||||||
|
|
||||||
Vue.component('vue-multiselect', window.VueMultiselect.default)
|
Vue.component('vue-multiselect', window.VueMultiselect.default)
|
||||||
import { ApiApiFactory } from "@/utils/openapi/api"
|
|
||||||
|
|
||||||
let app = new Vue({
|
let app = new Vue({
|
||||||
components: {
|
components: {
|
||||||
@@ -885,27 +886,27 @@
|
|||||||
this.$set(this.$refs.ingredient[index].$data, 'search', this.recipe_data.recipeIngredient[index].ingredient.text)
|
this.$set(this.$refs.ingredient[index].$data, 'search', this.recipe_data.recipeIngredient[index].ingredient.text)
|
||||||
},
|
},
|
||||||
searchKeywords: function (query) {
|
searchKeywords: function (query) {
|
||||||
// this.keywords_loading = true
|
|
||||||
// this.$http.get("{% url 'dal_keyword' %}" + '?q=' + query).then((response) => {
|
|
||||||
// this.keywords = response.data.results;
|
|
||||||
// this.keywords_loading = false
|
|
||||||
// }).catch((err) => {
|
|
||||||
// console.log(err)
|
|
||||||
// this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
|
|
||||||
// })
|
|
||||||
let apiFactory = new ApiApiFactory()
|
|
||||||
|
|
||||||
this.keywords_loading = true
|
this.keywords_loading = true
|
||||||
apiFactory
|
this.$http.get("{% url 'dal_keyword' %}" + '?q=' + query).then((response) => {
|
||||||
.listKeywords(query, undefined, undefined, 1, this.options_limit)
|
this.keywords = response.data.results;
|
||||||
.then((response) => {
|
this.keywords_loading = false
|
||||||
this.keywords = response.data.results
|
}).catch((err) => {
|
||||||
this.keywords_loading = false
|
console.log(err)
|
||||||
})
|
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
|
||||||
.catch((err) => {
|
})
|
||||||
console.log(err)
|
// let apiFactory = new ApiApiFactory()
|
||||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
|
|
||||||
})
|
// this.keywords_loading = true
|
||||||
|
// apiFactory
|
||||||
|
// .listKeywords(query, undefined, undefined, 1, this.options_limit)
|
||||||
|
// .then((response) => {
|
||||||
|
// this.keywords = response.data.results
|
||||||
|
// this.keywords_loading = false
|
||||||
|
// })
|
||||||
|
// .catch((err) => {
|
||||||
|
// console.log(err)
|
||||||
|
// StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
|
||||||
|
// })
|
||||||
},
|
},
|
||||||
searchUnits: function (query) {
|
searchUnits: function (query) {
|
||||||
let apiFactory = new ApiApiFactory()
|
let apiFactory = new ApiApiFactory()
|
||||||
@@ -932,46 +933,46 @@
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
searchIngredients: function (query) {
|
searchIngredients: function (query) {
|
||||||
// this.ingredients_loading = true
|
this.ingredients_loading = true
|
||||||
// this.$http.get("{% url 'dal_food' %}" + '?q=' + query).then((response) => {
|
this.$http.get("{% url 'dal_food' %}" + '?q=' + query).then((response) => {
|
||||||
// this.ingredients = response.data.results
|
this.ingredients = response.data.results
|
||||||
// if (this.recipe_data !== undefined) {
|
if (this.recipe_data !== undefined) {
|
||||||
// for (let x of Array.from(this.recipe_data.recipeIngredient)) {
|
for (let x of Array.from(this.recipe_data.recipeIngredient)) {
|
||||||
// if (x.ingredient.text !== '') {
|
if (x.ingredient.text !== '') {
|
||||||
// this.ingredients = this.ingredients.filter(item => item.text !== x.ingredient.text)
|
this.ingredients = this.ingredients.filter(item => item.text !== x.ingredient.text)
|
||||||
// this.ingredients.push(x.ingredient)
|
this.ingredients.push(x.ingredient)
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// this.ingredients_loading = false
|
|
||||||
// }).catch((err) => {
|
|
||||||
// console.log(err)
|
|
||||||
// this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
|
|
||||||
// })
|
|
||||||
let apiFactory = new ApiApiFactory()
|
|
||||||
|
|
||||||
this.foods_loading = true
|
|
||||||
apiFactory
|
|
||||||
.listFoods(query, undefined, undefined, 1, this.options_limit)
|
|
||||||
.then((response) => {
|
|
||||||
this.foods = response.data.results
|
|
||||||
|
|
||||||
if (this.recipe !== undefined) {
|
|
||||||
for (let s of this.recipe.steps) {
|
|
||||||
for (let i of s.ingredients) {
|
|
||||||
if (i.food !== null && i.food.id === undefined) {
|
|
||||||
this.foods.push(i.food)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.foods_loading = false
|
this.ingredients_loading = false
|
||||||
})
|
}).catch((err) => {
|
||||||
.catch((err) => {
|
console.log(err)
|
||||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
|
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
|
||||||
})
|
})
|
||||||
|
// let apiFactory = new ApiApiFactory()
|
||||||
|
|
||||||
|
// this.foods_loading = true
|
||||||
|
// apiFactory
|
||||||
|
// .listFoods(query, undefined, undefined, 1, this.options_limit)
|
||||||
|
// .then((response) => {
|
||||||
|
// this.foods = response.data.results
|
||||||
|
|
||||||
|
// if (this.recipe !== undefined) {
|
||||||
|
// for (let s of this.recipe.steps) {
|
||||||
|
// for (let i of s.ingredients) {
|
||||||
|
// if (i.food !== null && i.food.id === undefined) {
|
||||||
|
// this.foods.push(i.food)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// this.foods_loading = false
|
||||||
|
// })
|
||||||
|
// .catch((err) => {
|
||||||
|
// StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
|
||||||
|
// })
|
||||||
},
|
},
|
||||||
deleteNode: function (node, item, e) {
|
deleteNode: function (node, item, e) {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|||||||
@@ -655,7 +655,8 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
# self.queryset = search_recipes(self.request, self.queryset, self.request.GET)
|
# self.queryset = search_recipes(self.request, self.queryset, self.request.GET)
|
||||||
params = {x: self.request.GET.get(x) if len({**self.request.GET}[x]) == 1 else self.request.GET.getlist(x) for x in list(self.request.GET)}
|
params = {x: self.request.GET.get(x) if len({**self.request.GET}[x]) == 1 else self.request.GET.getlist(x) for x in list(self.request.GET)}
|
||||||
self.queryset = RecipeSearch(self.request, **params).get_queryset(self.queryset).prefetch_related('cooklog_set')
|
search = RecipeSearch(self.request, **params)
|
||||||
|
self.queryset = search.get_queryset(self.queryset).prefetch_related('cooklog_set')
|
||||||
return self.queryset
|
return self.queryset
|
||||||
|
|
||||||
def list(self, request, *args, **kwargs):
|
def list(self, request, *args, **kwargs):
|
||||||
|
|||||||
@@ -45,21 +45,15 @@ def hook(request, token):
|
|||||||
tb.save()
|
tb.save()
|
||||||
|
|
||||||
if tb.chat_id == str(data['message']['chat']['id']):
|
if tb.chat_id == str(data['message']['chat']['id']):
|
||||||
sl = ShoppingList.objects.filter(Q(created_by=tb.created_by)).filter(finished=False, space=tb.space).order_by('-created_at').first()
|
|
||||||
if not sl:
|
|
||||||
sl = ShoppingList.objects.create(created_by=tb.created_by, space=tb.space)
|
|
||||||
|
|
||||||
request.space = tb.space # TODO this is likely a bad idea. Verify and test
|
request.space = tb.space # TODO this is likely a bad idea. Verify and test
|
||||||
request.user = tb.created_by
|
request.user = tb.created_by
|
||||||
ingredient_parser = IngredientParser(request, False)
|
ingredient_parser = IngredientParser(request, False)
|
||||||
amount, unit, ingredient, note = ingredient_parser.parse(data['message']['text'])
|
amount, unit, ingredient, note = ingredient_parser.parse(data['message']['text'])
|
||||||
f = ingredient_parser.get_food(ingredient)
|
f = ingredient_parser.get_food(ingredient)
|
||||||
u = ingredient_parser.get_unit(unit)
|
u = ingredient_parser.get_unit(unit)
|
||||||
sl.entries.add(
|
|
||||||
ShoppingListEntry.objects.create(
|
ShoppingListEntry.objects.create(food=f, unit=u, amount=amount, created_by=request.user, space=request.space)
|
||||||
food=f, unit=u, amount=amount
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return JsonResponse({'data': data['message']['text']})
|
return JsonResponse({'data': data['message']['text']})
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -138,6 +138,8 @@ In both cases, also make sure to mount `/media/` in your swag container to point
|
|||||||
|
|
||||||
Please refer to the [appropriate documentation](https://github.com/linuxserver/docker-swag#usage) for the container setup.
|
Please refer to the [appropriate documentation](https://github.com/linuxserver/docker-swag#usage) for the container setup.
|
||||||
|
|
||||||
|
For step-by-step instructions to set this up from scratch, see [this example](swag.md).
|
||||||
|
|
||||||
### Others
|
### Others
|
||||||
|
|
||||||
If you use none of the above mentioned reverse proxies or want to use an existing one on your host machine (like a local nginx or Caddy), simply use the [PLAIN](https://docs.tandoor.dev/install/docker/#plain) setup above and change the outbound port to one of your liking.
|
If you use none of the above mentioned reverse proxies or want to use an existing one on your host machine (like a local nginx or Caddy), simply use the [PLAIN](https://docs.tandoor.dev/install/docker/#plain) setup above and change the outbound port to one of your liking.
|
||||||
|
|||||||
118
docs/install/swag.md
Normal file
118
docs/install/swag.md
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
!!! danger
|
||||||
|
Please refer to the [official documentation](https://github.com/linuxserver/docker-swag#usage) for the container setup. This example shows just one setup that may or may not differ from yours in significant ways. This tutorial does not cover security measures, backups, and many other things that you might want to consider.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- You have a newly spun-up Ubuntu server with docker (pre-)installed.
|
||||||
|
- At least one `mydomain.com` and one `mysubdomain.mydomain.com` are pointing to the server's IP. (This tutorial does not cover subfolder installation.)
|
||||||
|
- You have an ssh terminal session open.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Download and edit Tandoor configuration
|
||||||
|
|
||||||
|
```
|
||||||
|
cd /opt
|
||||||
|
mkdir recipes
|
||||||
|
cd recipes
|
||||||
|
wget https://raw.githubusercontent.com/vabene1111/recipes/develop/.env.template -O .env
|
||||||
|
base64 /dev/urandom | head -c50
|
||||||
|
```
|
||||||
|
Copy the response from that last command and paste the key into the `.env` file:
|
||||||
|
```
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
You'll also need to enter a Postgres password into the `.env` file. Then, save the file and exit the editor.
|
||||||
|
|
||||||
|
### Install and configure Docker Compose
|
||||||
|
|
||||||
|
In keeping with [these instructions](https://docs.linuxserver.io/general/docker-compose):
|
||||||
|
```
|
||||||
|
cd /opt
|
||||||
|
curl -L --fail https://raw.githubusercontent.com/linuxserver/docker-docker-compose/master/run.sh -o /usr/local/bin/docker-compose
|
||||||
|
chmod +x /usr/local/bin/docker-compose
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, create and edit the docker compose file.
|
||||||
|
|
||||||
|
```
|
||||||
|
nano docker-compose.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
Paste the following and adjust your domains, subdomains and time zone.
|
||||||
|
|
||||||
|
```
|
||||||
|
---
|
||||||
|
version: "2.1"
|
||||||
|
services:
|
||||||
|
swag:
|
||||||
|
image: ghcr.io/linuxserver/swag
|
||||||
|
container_name: swag
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
environment:
|
||||||
|
- PUID=1000
|
||||||
|
- PGID=1000
|
||||||
|
- TZ=Europe/Berlin # <---- EDIT THIS <---- <----
|
||||||
|
- URL=mydomain.com # <---- EDIT THIS <---- <----
|
||||||
|
- SUBDOMAINS=mysubdomain,myothersubdomain # <---- EDIT THIS <---- <----
|
||||||
|
- EXTRA_DOMAINS=myotherdomain.com # <---- EDIT THIS <---- <----
|
||||||
|
- VALIDATION=http
|
||||||
|
volumes:
|
||||||
|
- ./swag:/config
|
||||||
|
- ./recipes/media:/media
|
||||||
|
ports:
|
||||||
|
- 443:443
|
||||||
|
- 80:80
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
db_recipes:
|
||||||
|
restart: always
|
||||||
|
container_name: db_recipes
|
||||||
|
image: postgres:11-alpine
|
||||||
|
volumes:
|
||||||
|
- ./recipes/db:/var/lib/postgresql/data
|
||||||
|
env_file:
|
||||||
|
- ./recipes/.env
|
||||||
|
|
||||||
|
recipes:
|
||||||
|
image: vabene1111/recipes
|
||||||
|
container_name: recipes
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- ./recipes/.env
|
||||||
|
environment:
|
||||||
|
- UID=1000
|
||||||
|
- GID=1000
|
||||||
|
- TZ=Europe/Berlin # <---- EDIT THIS <---- <----
|
||||||
|
volumes:
|
||||||
|
- ./recipes/static:/opt/recipes/staticfiles
|
||||||
|
- ./recipes/media:/opt/recipes/mediafiles
|
||||||
|
depends_on:
|
||||||
|
- db_recipes
|
||||||
|
```
|
||||||
|
|
||||||
|
Save and exit.
|
||||||
|
|
||||||
|
### Create containers and configure swag reverse proxy
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
cd /opt/swag/nginx/proxy-confs
|
||||||
|
cp recipes.subdomain.conf.sample recipes.subdomain.conf
|
||||||
|
nano recipes.subdomain.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
Change the line `server_name recipes.*;` to `server_name mysubdomain.*;`, save and exit.
|
||||||
|
|
||||||
|
### Finalize
|
||||||
|
|
||||||
|
```
|
||||||
|
cd /opt
|
||||||
|
docker restart swag recipes
|
||||||
|
```
|
||||||
|
|
||||||
|
Go to `https://mysubdomain.mydomain.com`. (If you get a "502 Bad Gateway" error, be patient. It might take a short while until it's functional.)
|
||||||
@@ -586,6 +586,8 @@ export default {
|
|||||||
if (this.recipe.working_time === "" || isNaN(this.recipe.working_time)) {
|
if (this.recipe.working_time === "" || isNaN(this.recipe.working_time)) {
|
||||||
this.recipe.working_time = 0
|
this.recipe.working_time = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.recipe.servings = Math.floor(this.recipe.servings) // temporary fix until a proper framework for frontend input validation is established
|
||||||
if (this.recipe.servings === "" || isNaN(this.recipe.servings)) {
|
if (this.recipe.servings === "" || isNaN(this.recipe.servings)) {
|
||||||
this.recipe.servings = 0
|
this.recipe.servings = 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app" style="margin-bottom: 4vh">
|
<div id="app" style="margin-bottom: 4vh">
|
||||||
<RecipeSwitcher mode="mealplan" />
|
<RecipeSwitcher ref="ref_recipe_switcher"/>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-xl-8 col-lg-10 offset-xl-2 offset-lg-1">
|
<div class="col-12 col-xl-8 col-lg-10 offset-xl-2 offset-lg-1">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-if="!loading">
|
<div v-if="!loading">
|
||||||
<RecipeSwitcher :recipe="rootrecipe.id" :name="rootrecipe.name" mode="recipe" @switch="quickSwitch($event)" />
|
<RecipeSwitcher ref="ref_recipe_switcher" @switch="quickSwitch($event)" />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12" style="text-align: center">
|
<div class="col-12" style="text-align: center">
|
||||||
<h3>{{ recipe.name }}</h3>
|
<h3>{{ recipe.name }}</h3>
|
||||||
|
|||||||
@@ -1,45 +1,75 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="recipes !== {}">
|
<div v-if="recipes !== {}">
|
||||||
<div id="switcher" class="align-center">
|
<div id="switcher" class="align-center">
|
||||||
<i class="btn btn-outline-dark fas fa-receipt fa-xl fa-fw shadow-none btn-circle"
|
<i class="btn btn-primary fas fa-receipt fa-xl fa-fw shadow-none btn-circle"
|
||||||
v-b-toggle.related-recipes/>
|
v-b-toggle.related-recipes/>
|
||||||
</div>
|
</div>
|
||||||
<b-sidebar id="related-recipes" title="Quick actions" backdrop right shadow="sm" style="z-index: 10000">
|
<b-sidebar id="related-recipes" backdrop right bottom no-header shadow="sm" style="z-index: 10000"
|
||||||
|
@shown="updatePinnedRecipes()">
|
||||||
<template #default="{ hide }">
|
<template #default="{ hide }">
|
||||||
|
|
||||||
<nav class="mb-3 ml-3">
|
<div class="d-flex flex-column justify-content-end h-100 p-3 align-items-end">
|
||||||
<b-nav vertical>
|
|
||||||
<h5><i class="fas fa-calendar fa-fw"></i> Planned</h5>
|
|
||||||
|
|
||||||
<div v-for="r in planned_recipes" :key="`plan${r.id}`">
|
<h5>Planned <i class="fas fa-calendar fa-fw"></i></h5>
|
||||||
<b-nav-item variant="link" @click="
|
|
||||||
navRecipe(r)
|
|
||||||
hide()
|
|
||||||
">{{ r.name }}
|
|
||||||
</b-nav-item>
|
|
||||||
</div>
|
|
||||||
<hr/>
|
|
||||||
<h5><i class="fas fa-thumbtack fa-fw"></i> Pinned</h5>
|
|
||||||
|
|
||||||
<div v-for="r in pinned_recipes" :key="`pin${r.id}`">
|
<div class="text-right">
|
||||||
<b-nav-item variant="link" @click="
|
<template v-if="planned_recipes.length > 0">
|
||||||
navRecipe(r)
|
<div v-for="r in planned_recipes" :key="`plan${r.id}`">
|
||||||
hide()
|
<div class="pb-1 pt-1">
|
||||||
">{{ r.name }}
|
<a @click=" navRecipe(r); hide()" href="javascript:void(0);">{{ r.name }}</a>
|
||||||
</b-nav-item>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr/>
|
</template>
|
||||||
<h5><i class="fas fa-link fa-fw"></i> Related</h5>
|
<template v-else>
|
||||||
|
<span class="text-muted">You have nothing planned for today!</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-for="r in related_recipes" :key="`related${r.id}`">
|
<h5>Pinned <i class="fas fa-thumbtack fa-fw"></i></h5>
|
||||||
<b-nav-item variant="link" @click="
|
|
||||||
navRecipe(r)
|
<template v-if="pinned_recipes.length > 0">
|
||||||
hide()
|
<div class="text-right">
|
||||||
">{{ r.name }}
|
<div v-for="r in pinned_recipes" :key="`pin${r.id}`">
|
||||||
</b-nav-item>
|
<b-row class="pb-1 pt-1">
|
||||||
|
<b-col cols="2">
|
||||||
|
<a href="javascript:void(0)" @click="unPinRecipe(r)"
|
||||||
|
class="text-muted"><i class="fas fa-times"></i></a>
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="10">
|
||||||
|
<a @click="navRecipe(r); hide()" href="javascript:void(0);"
|
||||||
|
class="align-self-end">{{ r.name }} </a>
|
||||||
|
</b-col>
|
||||||
|
|
||||||
|
</b-row>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</b-nav>
|
</template>
|
||||||
</nav>
|
<template v-else>
|
||||||
|
<span class="text-muted">You have no pinned recipes!</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<template v-if="related_recipes.length > 0">
|
||||||
|
<h5>Related <i class="fas fa-link fa-fw"></i></h5>
|
||||||
|
<div class="text-right">
|
||||||
|
<div v-for="r in related_recipes" :key="`related${r.id}`">
|
||||||
|
<div class="pb-1 pt-1">
|
||||||
|
<a @click=" navRecipe(r); hide()" href="javascript:void(0);">{{ r.name }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</template>
|
||||||
|
<template #footer="{ hide }">
|
||||||
|
<div class="d-flex bg-dark text-light align-items-center px-3 py-2">
|
||||||
|
<strong class="mr-auto">Quick actions</strong>
|
||||||
|
<b-button size="sm" @click="hide">Close</b-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</b-sidebar>
|
</b-sidebar>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,7 +90,7 @@ export default {
|
|||||||
related_recipes: [],
|
related_recipes: [],
|
||||||
planned_recipes: [],
|
planned_recipes: [],
|
||||||
pinned_recipes: [],
|
pinned_recipes: [],
|
||||||
recipes: {}
|
recipes: {},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -89,14 +119,22 @@ export default {
|
|||||||
window.location.href = this.resolveDjangoUrl("view_recipe", recipe.id)
|
window.location.href = this.resolveDjangoUrl("view_recipe", recipe.id)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
updatePinnedRecipes: function () {
|
||||||
|
//TODO clean this up to prevent duplicate API calls
|
||||||
|
this.loadPinnedRecipes()
|
||||||
|
this.loadRecipeData()
|
||||||
|
},
|
||||||
loadRecipeData: function () {
|
loadRecipeData: function () {
|
||||||
let apiClient = new ApiApiFactory()
|
let apiClient = new ApiApiFactory()
|
||||||
|
|
||||||
let recipe_list = [...this.related_recipes, ...this.planned_recipes, ...this.pinned_recipes]
|
let recipe_list = [...this.related_recipes, ...this.planned_recipes, ...this.pinned_recipes]
|
||||||
|
|
||||||
let recipe_ids = []
|
let recipe_ids = []
|
||||||
recipe_list.forEach((recipe) => {
|
recipe_list.forEach((recipe) => {
|
||||||
if (!recipe_ids.includes(recipe.id)) {
|
let id = recipe.id
|
||||||
recipe_ids.push(recipe.id)
|
|
||||||
|
if (!recipe_ids.includes(id)) {
|
||||||
|
recipe_ids.push(id)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -111,12 +149,15 @@ export default {
|
|||||||
let apiClient = new ApiApiFactory()
|
let apiClient = new ApiApiFactory()
|
||||||
|
|
||||||
// get related recipes and save them for later
|
// get related recipes and save them for later
|
||||||
return apiClient.relatedRecipe(this.recipe, {query: {levels: 2}}).then((result) => {
|
if (this.$parent.recipe) {
|
||||||
this.related_recipes = result.data
|
this.related_recipes = [this.$parent.recipe]
|
||||||
})
|
return apiClient.relatedRecipe(this.$parent.recipe.id, {query: {levels: 2, format: 'json'}}).then((result) => {
|
||||||
|
this.related_recipes = this.related_recipes.concat(result.data)
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
loadPinnedRecipes: function () {
|
loadPinnedRecipes: function () {
|
||||||
let pinned_recipe_ids = localStorage.getItem('pinned_recipes') || []
|
let pinned_recipe_ids = JSON.parse(localStorage.getItem('pinned_recipes')) || []
|
||||||
this.pinned_recipes = pinned_recipe_ids
|
this.pinned_recipes = pinned_recipe_ids
|
||||||
},
|
},
|
||||||
loadMealPlans: function () {
|
loadMealPlans: function () {
|
||||||
@@ -142,6 +183,13 @@ export default {
|
|||||||
return Promise.all(promises)
|
return Promise.all(promises)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
unPinRecipe: function (recipe) {
|
||||||
|
let pinnedRecipes = JSON.parse(localStorage.getItem('pinned_recipes')) || []
|
||||||
|
pinnedRecipes = pinnedRecipes.filter((r) => r.id !== recipe.id)
|
||||||
|
console.log('pinned left', pinnedRecipes)
|
||||||
|
this.pinned_recipes = pinnedRecipes
|
||||||
|
localStorage.setItem('pinned_recipes', JSON.stringify(pinnedRecipes))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -8,12 +8,12 @@
|
|||||||
:class="{ 'border border-primary': over, shake: isError }"
|
:class="{ 'border border-primary': over, shake: isError }"
|
||||||
:style="{ 'cursor:grab': useDrag }"
|
:style="{ 'cursor:grab': useDrag }"
|
||||||
:draggable="useDrag"
|
:draggable="useDrag"
|
||||||
@[useDrag&&`dragover`].prevent
|
@[useDrag&&`dragover`||``].prevent
|
||||||
@[useDrag&&`dragenter`].prevent
|
@[useDrag&&`dragenter`||``].prevent
|
||||||
@[useDrag&&`dragstart`]="handleDragStart($event)"
|
@[useDrag&&`dragstart`||``]="handleDragStart($event)"
|
||||||
@[useDrag&&`dragenter`]="handleDragEnter($event)"
|
@[useDrag&&`dragenter`||``]="handleDragEnter($event)"
|
||||||
@[useDrag&&`dragleave`]="handleDragLeave($event)"
|
@[useDrag&&`dragleave`||``]="handleDragLeave($event)"
|
||||||
@[useDrag&&`drop`]="handleDragDrop($event)"
|
@[useDrag&&`drop`||``]="handleDragDrop($event)"
|
||||||
>
|
>
|
||||||
<b-row no-gutters>
|
<b-row no-gutters>
|
||||||
<b-col no-gutters class="col-sm-3">
|
<b-col no-gutters class="col-sm-3">
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
<div class="m-0 text-truncate small text-muted" v-if="getFullname">{{ getFullname }}</div>
|
<div class="m-0 text-truncate small text-muted" v-if="getFullname">{{ getFullname }}</div>
|
||||||
|
|
||||||
<generic-pill v-for="x in itemTags" :key="x.field" :item_list="itemList(x)" :label="x.label" :color="x.color" />
|
<generic-pill v-for="x in itemTags" :key="x.field" :item_list="itemList(x)" :label="x.label" :color="x.color" />
|
||||||
|
|
||||||
<generic-ordered-pill
|
<generic-ordered-pill
|
||||||
v-for="x in itemOrderedTags"
|
v-for="x in itemOrderedTags"
|
||||||
:key="x.field"
|
:key="x.field"
|
||||||
@@ -37,6 +38,7 @@
|
|||||||
:item="item"
|
:item="item"
|
||||||
@finish-action="finishAction"
|
@finish-action="finishAction"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="mt-auto mb-1" align="right">
|
<div class="mt-auto mb-1" align="right">
|
||||||
<span 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 })">
|
<span 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] }} {{ itemName }}</div>
|
<div v-if="!item.show_children">{{ item[child_count] }} {{ itemName }}</div>
|
||||||
|
|||||||
@@ -1,173 +1,172 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<b-form-group :class="class_list">
|
<b-form-group class="mb-3">
|
||||||
<template #label v-if="show_label">
|
<template #label v-if="show_label">
|
||||||
{{ form.label }}
|
{{ form.label }}
|
||||||
</template>
|
</template>
|
||||||
<generic-multiselect
|
<generic-multiselect
|
||||||
@change="new_value = $event.val"
|
@change="new_value = $event.val"
|
||||||
@remove="new_value = undefined"
|
@remove="new_value = undefined"
|
||||||
:initial_selection="initialSelection"
|
:initial_selection="initialSelection"
|
||||||
:model="model"
|
:model="model"
|
||||||
:multiple="useMultiple"
|
:multiple="useMultiple"
|
||||||
:sticky_options="sticky_options"
|
:sticky_options="sticky_options"
|
||||||
:allow_create="form.allow_create"
|
:allow_create="form.allow_create"
|
||||||
:create_placeholder="createPlaceholder"
|
:create_placeholder="createPlaceholder"
|
||||||
:clear="clear"
|
:clear="clear"
|
||||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||||
:placeholder="modelName"
|
:placeholder="modelName"
|
||||||
@new="addNew"
|
@new="addNew"
|
||||||
>
|
>
|
||||||
</generic-multiselect>
|
</generic-multiselect>
|
||||||
</b-form-group>
|
</b-form-group>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import GenericMultiselect from "@/components/GenericMultiselect"
|
import GenericMultiselect from "@/components/GenericMultiselect"
|
||||||
import {StandardToasts, ApiMixin} from "@/utils/utils"
|
import { StandardToasts, ApiMixin } from "@/utils/utils"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "LookupInput",
|
name: "LookupInput",
|
||||||
components: {GenericMultiselect},
|
components: { GenericMultiselect },
|
||||||
mixins: [ApiMixin],
|
mixins: [ApiMixin],
|
||||||
props: {
|
props: {
|
||||||
form: {
|
form: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default() {
|
default() {
|
||||||
return undefined
|
return undefined
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
model: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return undefined
|
||||||
|
},
|
||||||
|
},
|
||||||
|
show_label: { type: Boolean, default: true },
|
||||||
|
clear: { type: Number },
|
||||||
},
|
},
|
||||||
model: {
|
data() {
|
||||||
type: Object,
|
return {
|
||||||
default() {
|
new_value: undefined,
|
||||||
return undefined
|
field: undefined,
|
||||||
},
|
label: undefined,
|
||||||
},
|
sticky_options: undefined,
|
||||||
class_list: {type: String, default: "mb-3"},
|
first_run: true,
|
||||||
show_label: {type: Boolean, default: true},
|
|
||||||
clear: {type: Number},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
new_value: undefined,
|
|
||||||
field: undefined,
|
|
||||||
label: undefined,
|
|
||||||
sticky_options: undefined,
|
|
||||||
first_run: true,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.new_value = this.form?.value
|
|
||||||
this.field = this.form?.field ?? "You Forgot To Set Field Name"
|
|
||||||
this.label = this.form?.label ?? ""
|
|
||||||
this.sticky_options = this.form?.sticky_options ?? []
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
modelName() {
|
|
||||||
return this?.model?.name ?? this.$t("Search")
|
|
||||||
},
|
|
||||||
useMultiple() {
|
|
||||||
return this.form?.multiple || this.form?.ordered || false
|
|
||||||
},
|
|
||||||
initialSelection() {
|
|
||||||
let this_value = this.new_value
|
|
||||||
let arrayValues = undefined
|
|
||||||
// multiselect is expect to get an array of objects - make sure it gets one
|
|
||||||
if (Array.isArray(this_value)) {
|
|
||||||
arrayValues = this_value
|
|
||||||
} else if (!this_value) {
|
|
||||||
arrayValues = []
|
|
||||||
} else if (typeof this_value === "object") {
|
|
||||||
arrayValues = [this_value]
|
|
||||||
} else {
|
|
||||||
arrayValues = [{id: -1, name: this_value}]
|
|
||||||
}
|
|
||||||
if (this.form?.ordered && this.first_run && arrayValues.length > 0) {
|
|
||||||
return this.flattenItems(arrayValues)
|
|
||||||
} else {
|
|
||||||
return arrayValues
|
|
||||||
}
|
|
||||||
},
|
|
||||||
createPlaceholder() {
|
|
||||||
return this.$t("Create_New_" + this?.model?.name)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
"form.value": function (newVal, oldVal) {
|
|
||||||
this.new_value = newVal
|
|
||||||
},
|
|
||||||
new_value: function () {
|
|
||||||
let x = this?.new_value
|
|
||||||
// pass the unflattened attributes that can be restored when ready to save/update
|
|
||||||
if (this.form?.ordered) {
|
|
||||||
x["__override__"] = this.unflattenItem(this?.new_value)
|
|
||||||
}
|
|
||||||
this.$root.$emit("change", this.form.field, x)
|
|
||||||
this.$emit("change", x)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
addNew: function (e) {
|
|
||||||
// if create a new item requires more than 1 parameter or the field 'name' is insufficient this will need reworked
|
|
||||||
// in a perfect world this would trigger a new modal and allow editing all fields
|
|
||||||
this.genericAPI(this.model, this.Actions.CREATE, {name: e})
|
|
||||||
.then((result) => {
|
|
||||||
this.new_value = result.data
|
|
||||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
// ordered lookups have nested attributes that need flattened attributes to drive lookup
|
|
||||||
flattenItems: function (itemlist) {
|
|
||||||
let flat_items = []
|
|
||||||
let item = undefined
|
|
||||||
let label = this.form.list_label.split("::")
|
|
||||||
itemlist.forEach((x) => {
|
|
||||||
item = {}
|
|
||||||
for (const [k, v] of Object.entries(x)) {
|
|
||||||
if (k == label[0]) {
|
|
||||||
item["id"] = v.id
|
|
||||||
item[label[1]] = v[label[1]]
|
|
||||||
} else {
|
|
||||||
item[this.form.field + "__" + k] = v
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
flat_items.push(item)
|
|
||||||
})
|
|
||||||
this.first_run = false
|
|
||||||
return flat_items
|
|
||||||
},
|
},
|
||||||
unflattenItem: function (itemList) {
|
mounted() {
|
||||||
let unflat_items = []
|
this.new_value = this.form?.value
|
||||||
let item = undefined
|
this.field = this.form?.field ?? "You Forgot To Set Field Name"
|
||||||
let this_label = undefined
|
this.label = this.form?.label ?? ""
|
||||||
let label = this.form.list_label.split("::")
|
this.sticky_options = this.form?.sticky_options ?? []
|
||||||
let order = 0
|
},
|
||||||
itemList.forEach((x) => {
|
computed: {
|
||||||
item = {}
|
modelName() {
|
||||||
item[label[0]] = {}
|
return this?.model?.name ?? this.$t("Search")
|
||||||
for (const [k, v] of Object.entries(x)) {
|
},
|
||||||
switch (k) {
|
useMultiple() {
|
||||||
case "id":
|
return this.form?.multiple || this.form?.ordered || false
|
||||||
item[label[0]]["id"] = v
|
},
|
||||||
break
|
initialSelection() {
|
||||||
case label[1]:
|
let this_value = this.new_value
|
||||||
item[label[0]][label[1]] = v
|
let arrayValues = undefined
|
||||||
break
|
// multiselect is expect to get an array of objects - make sure it gets one
|
||||||
default:
|
if (Array.isArray(this_value)) {
|
||||||
this_label = k.replace(this.form.field + "__", "")
|
arrayValues = this_value
|
||||||
}
|
} else if (!this_value) {
|
||||||
}
|
arrayValues = []
|
||||||
item["order"] = order
|
} else if (typeof this_value === "object") {
|
||||||
order++
|
arrayValues = [this_value]
|
||||||
unflat_items.push(item)
|
} else {
|
||||||
})
|
arrayValues = [{ id: -1, name: this_value }]
|
||||||
return unflat_items
|
}
|
||||||
|
if (this.form?.ordered && this.first_run) {
|
||||||
|
return this.flattenItems(arrayValues)
|
||||||
|
} else {
|
||||||
|
return arrayValues
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createPlaceholder() {
|
||||||
|
return this.$t("Create_New_" + this?.model?.name)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
"form.value": function (newVal, oldVal) {
|
||||||
|
this.new_value = newVal
|
||||||
|
},
|
||||||
|
new_value: function () {
|
||||||
|
let x = this?.new_value
|
||||||
|
// pass the unflattened attributes that can be restored when ready to save/update
|
||||||
|
if (this.form?.ordered) {
|
||||||
|
x["__override__"] = this.unflattenItem(this?.new_value)
|
||||||
|
}
|
||||||
|
this.$root.$emit("change", this.form.field, x)
|
||||||
|
this.$emit("change", x)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
addNew: function (e) {
|
||||||
|
// if create a new item requires more than 1 parameter or the field 'name' is insufficient this will need reworked
|
||||||
|
// in a perfect world this would trigger a new modal and allow editing all fields
|
||||||
|
this.genericAPI(this.model, this.Actions.CREATE, { name: e })
|
||||||
|
.then((result) => {
|
||||||
|
this.new_value = result.data
|
||||||
|
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// ordered lookups have nested attributes that need flattened attributes to drive lookup
|
||||||
|
flattenItems: function (itemlist) {
|
||||||
|
let flat_items = []
|
||||||
|
let item = undefined
|
||||||
|
let label = this.form.list_label.split("::")
|
||||||
|
itemlist.forEach((x) => {
|
||||||
|
item = {}
|
||||||
|
for (const [k, v] of Object.entries(x)) {
|
||||||
|
if (k == label[0]) {
|
||||||
|
item["id"] = v.id
|
||||||
|
item[label[1]] = v[label[1]]
|
||||||
|
} else {
|
||||||
|
item[this.form.field + "__" + k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flat_items.push(item)
|
||||||
|
})
|
||||||
|
this.first_run = false
|
||||||
|
return flat_items
|
||||||
|
},
|
||||||
|
unflattenItem: function (itemList) {
|
||||||
|
let unflat_items = []
|
||||||
|
let item = undefined
|
||||||
|
let this_label = undefined
|
||||||
|
let label = this.form.list_label.split("::")
|
||||||
|
let order = 0
|
||||||
|
itemList.forEach((x) => {
|
||||||
|
item = {}
|
||||||
|
item[label[0]] = {}
|
||||||
|
for (const [k, v] of Object.entries(x)) {
|
||||||
|
switch (k) {
|
||||||
|
case "id":
|
||||||
|
item[label[0]]["id"] = v
|
||||||
|
break
|
||||||
|
case label[1]:
|
||||||
|
item[label[0]][label[1]] = v
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
this_label = k.replace(this.form.field + "__", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item["order"] = order
|
||||||
|
order++
|
||||||
|
unflat_items.push(item)
|
||||||
|
})
|
||||||
|
return unflat_items
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,38 +1,60 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="dropdown d-print-none">
|
<div class="dropdown d-print-none">
|
||||||
<a class="btn shadow-none" href="javascript:void(0);" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
<a class="btn shadow-none" href="javascript:void(0);" role="button" id="dropdownMenuLink"
|
||||||
|
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
<i class="fas fa-ellipsis-v fa-lg"></i>
|
<i class="fas fa-ellipsis-v fa-lg"></i>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuLink">
|
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuLink">
|
||||||
<a class="dropdown-item" :href="resolveDjangoUrl('edit_recipe', recipe.id)"><i class="fas fa-pencil-alt fa-fw"></i> {{ $t("Edit") }}</a>
|
<a class="dropdown-item" :href="resolveDjangoUrl('edit_recipe', recipe.id)"><i
|
||||||
|
class="fas fa-pencil-alt fa-fw"></i> {{ $t("Edit") }}</a>
|
||||||
|
|
||||||
<a class="dropdown-item" :href="resolveDjangoUrl('edit_convert_recipe', recipe.id)" v-if="!recipe.internal"><i class="fas fa-exchange-alt fa-fw"></i> {{ $t("convert_internal") }}</a>
|
<a class="dropdown-item" :href="resolveDjangoUrl('edit_convert_recipe', recipe.id)"
|
||||||
|
v-if="!recipe.internal"><i class="fas fa-exchange-alt fa-fw"></i> {{ $t("convert_internal") }}</a>
|
||||||
|
|
||||||
<a href="javascript:void(0);">
|
<a href="javascript:void(0);">
|
||||||
<button class="dropdown-item" @click="$bvModal.show(`id_modal_add_book_${modal_id}`)"><i class="fas fa-bookmark fa-fw"></i> {{ $t("Manage_Books") }}</button>
|
<button class="dropdown-item" @click="$bvModal.show(`id_modal_add_book_${modal_id}`)"><i
|
||||||
|
class="fas fa-bookmark fa-fw"></i> {{ $t("Manage_Books") }}
|
||||||
|
</button>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a class="dropdown-item" :href="`${resolveDjangoUrl('view_shopping')}?r=[${recipe.id},${servings_value}]`" v-if="recipe.internal" target="_blank" rel="noopener noreferrer">
|
<a class="dropdown-item"
|
||||||
|
:href="`${resolveDjangoUrl('view_shopping')}?r=[${recipe.id},${servings_value}]`"
|
||||||
|
v-if="recipe.internal" target="_blank" rel="noopener noreferrer">
|
||||||
<i class="fas fa-shopping-cart fa-fw"></i> {{ $t("Add_to_Shopping") }}
|
<i class="fas fa-shopping-cart fa-fw"></i> {{ $t("Add_to_Shopping") }}
|
||||||
</a>
|
</a>
|
||||||
<a class="dropdown-item" v-if="recipe.internal" @click="addToShopping" href="#"> <i class="fas fa-shopping-cart fa-fw"></i> {{ $t("create_shopping_new") }} </a>
|
<a class="dropdown-item" v-if="recipe.internal" @click="addToShopping" href="#"> <i
|
||||||
|
class="fas fa-shopping-cart fa-fw"></i> {{ $t("create_shopping_new") }} </a>
|
||||||
|
|
||||||
<a class="dropdown-item" @click="createMealPlan" href="javascript:void(0);"><i class="fas fa-calendar fa-fw"></i> {{ $t("Add_to_Plan") }} </a>
|
<a class="dropdown-item" @click="createMealPlan" href="javascript:void(0);"><i
|
||||||
|
class="fas fa-calendar fa-fw"></i> {{ $t("Add_to_Plan") }} </a>
|
||||||
|
|
||||||
<a href="javascript:void(0);">
|
<a href="javascript:void(0);">
|
||||||
<button class="dropdown-item" @click="$bvModal.show(`id_modal_cook_log_${modal_id}`)"><i class="fas fa-clipboard-list fa-fw"></i> {{ $t("Log_Cooking") }}</button>
|
<button class="dropdown-item" @click="$bvModal.show(`id_modal_cook_log_${modal_id}`)"><i
|
||||||
|
class="fas fa-clipboard-list fa-fw"></i> {{ $t("Log_Cooking") }}
|
||||||
|
</button>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="javascript:void(0);">
|
<a href="javascript:void(0);">
|
||||||
<button class="dropdown-item" onclick="window.print()"><i class="fas fa-print fa-fw"></i> {{ $t("Print") }}</button>
|
<button class="dropdown-item" onclick="window.print()"><i class="fas fa-print fa-fw"></i>
|
||||||
|
{{ $t("Print") }}
|
||||||
|
</button>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a class="dropdown-item" :href="resolveDjangoUrl('view_export') + '?r=' + recipe.id" target="_blank" rel="noopener noreferrer"><i class="fas fa-file-export fa-fw"></i> {{ $t("Export") }}</a>
|
<a class="dropdown-item" :href="resolveDjangoUrl('view_export') + '?r=' + recipe.id" target="_blank"
|
||||||
|
rel="noopener noreferrer"><i class="fas fa-file-export fa-fw"></i> {{ $t("Export") }}</a>
|
||||||
|
|
||||||
<a href="javascript:void(0);">
|
<a href="javascript:void(0);">
|
||||||
<button class="dropdown-item" @click="createShareLink()" v-if="recipe.internal"><i class="fas fa-share-alt fa-fw"></i> {{ $t("Share") }}</button>
|
<button class="dropdown-item" @click="pinRecipe()"><i class="fas fa-thumbtack fa-fw"></i>
|
||||||
|
{{ $t("Pin") }}
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="javascript:void(0);">
|
||||||
|
<button class="dropdown-item" @click="createShareLink()" v-if="recipe.internal"><i
|
||||||
|
class="fas fa-share-alt fa-fw"></i> {{ $t("Share") }}
|
||||||
|
</button>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,10 +66,17 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-12">
|
<div class="col col-md-12">
|
||||||
<label v-if="recipe_share_link !== undefined">{{ $t("Public share link") }}</label>
|
<label v-if="recipe_share_link !== undefined">{{ $t("Public share link") }}</label>
|
||||||
<input ref="share_link_ref" class="form-control" v-model="recipe_share_link" />
|
<input ref="share_link_ref" class="form-control" v-model="recipe_share_link"/>
|
||||||
<b-button class="mt-2 mb-3 d-none d-md-inline" variant="secondary" @click="$bvModal.hide(`modal-share-link_${modal_id}`)">{{ $t("Close") }} </b-button>
|
<b-button class="mt-2 mb-3 d-none d-md-inline" variant="secondary"
|
||||||
<b-button class="mt-2 mb-3 ml-md-2" variant="primary" @click="copyShareLink()">{{ $t("Copy") }}</b-button>
|
@click="$bvModal.hide(`modal-share-link_${modal_id}`)">{{ $t("Close") }}
|
||||||
<b-button class="mt-2 mb-3 ml-2 float-right" variant="success" @click="shareIntend()">{{ $t("Share") }} <i class="fa fa-share-alt"></i></b-button>
|
</b-button>
|
||||||
|
<b-button class="mt-2 mb-3 ml-md-2" variant="primary" @click="copyShareLink()">{{
|
||||||
|
$t("Copy")
|
||||||
|
}}
|
||||||
|
</b-button>
|
||||||
|
<b-button class="mt-2 mb-3 ml-2 float-right" variant="success" @click="shareIntend()">{{
|
||||||
|
$t("Share")
|
||||||
|
}} <i class="fa fa-share-alt"></i></b-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</b-modal>
|
</b-modal>
|
||||||
@@ -62,12 +91,12 @@
|
|||||||
:allow_delete="false"
|
:allow_delete="false"
|
||||||
:modal_title="$t('Create_Meal_Plan_Entry')"
|
:modal_title="$t('Create_Meal_Plan_Entry')"
|
||||||
></meal-plan-edit-modal>
|
></meal-plan-edit-modal>
|
||||||
<shopping-modal :recipe="recipe" :servings="servings_value" :modal_id="modal_id" />
|
<shopping-modal :recipe="recipe" :servings="servings_value" :modal_id="modal_id"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { makeToast, resolveDjangoUrl, ResolveUrlMixin, StandardToasts } from "@/utils/utils"
|
import {makeToast, resolveDjangoUrl, ResolveUrlMixin, StandardToasts} from "@/utils/utils"
|
||||||
import CookLog from "@/components/CookLog"
|
import CookLog from "@/components/CookLog"
|
||||||
import axios from "axios"
|
import axios from "axios"
|
||||||
import AddRecipeToBook from "@/components/Modals/AddRecipeToBook"
|
import AddRecipeToBook from "@/components/Modals/AddRecipeToBook"
|
||||||
@@ -75,7 +104,7 @@ import MealPlanEditModal from "@/components/MealPlanEditModal"
|
|||||||
import ShoppingModal from "@/components/Modals/ShoppingModal"
|
import ShoppingModal from "@/components/Modals/ShoppingModal"
|
||||||
import moment from "moment"
|
import moment from "moment"
|
||||||
import Vue from "vue"
|
import Vue from "vue"
|
||||||
import { ApiApiFactory } from "@/utils/openapi/api"
|
import {ApiApiFactory} from "@/utils/openapi/api"
|
||||||
|
|
||||||
Vue.prototype.moment = moment
|
Vue.prototype.moment = moment
|
||||||
|
|
||||||
@@ -121,6 +150,11 @@ export default {
|
|||||||
this.servings_value = this.servings === -1 ? this.recipe.servings : this.servings
|
this.servings_value = this.servings === -1 ? this.recipe.servings : this.servings
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
pinRecipe: function () {
|
||||||
|
let pinnedRecipes = JSON.parse(localStorage.getItem('pinned_recipes')) || []
|
||||||
|
pinnedRecipes.push({id: this.recipe.id, name: this.recipe.name})
|
||||||
|
localStorage.setItem('pinned_recipes', JSON.stringify(pinnedRecipes))
|
||||||
|
},
|
||||||
saveMealPlan: function (entry) {
|
saveMealPlan: function (entry) {
|
||||||
entry.date = moment(entry.date).format("YYYY-MM-DD")
|
entry.date = moment(entry.date).format("YYYY-MM-DD")
|
||||||
|
|
||||||
|
|||||||
@@ -277,14 +277,11 @@
|
|||||||
"copy_markdown_table": "Copy as Markdown Table",
|
"copy_markdown_table": "Copy as Markdown Table",
|
||||||
"in_shopping": "In Shopping List",
|
"in_shopping": "In Shopping List",
|
||||||
"DelayUntil": "Delay Until",
|
"DelayUntil": "Delay Until",
|
||||||
|
"Pin": "Pin",
|
||||||
"mark_complete": "Mark Complete",
|
"mark_complete": "Mark Complete",
|
||||||
"QuickEntry": "Quick Entry",
|
"QuickEntry": "Quick Entry",
|
||||||
"shopping_add_onhand_desc": "Mark food 'On Hand' when checked off shopping list.",
|
"shopping_add_onhand_desc": "Mark food 'On Hand' when checked off shopping list.",
|
||||||
"shopping_add_onhand": "Auto On Hand",
|
"shopping_add_onhand": "Auto On Hand",
|
||||||
"related_recipes": "Related Recipes",
|
"related_recipes": "Related Recipes",
|
||||||
"today_recipes": "Today's Recipes",
|
"today_recipes": "Today's Recipes"
|
||||||
"mark_complete": "Mark Complete",
|
|
||||||
"QuickEntry": "Quick Entry",
|
|
||||||
"shopping_add_onhand_desc": "Mark food 'On Hand' when checked off shopping list.",
|
|
||||||
"shopping_add_onhand": "Auto On Hand"
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user