Merge branch 'develop' into feature/vue3

# Conflicts:
#	recipes/settings.py
#	vue/vue.config.js
This commit is contained in:
vabene1111
2024-02-28 17:13:25 +01:00
34 changed files with 659 additions and 1497 deletions

View File

@@ -13,7 +13,7 @@ from cookbook.managers import DICTIONARY
from .models import (BookmarkletImport, Comment, CookLog, Food, ImportLog, Ingredient, InviteLink, from .models import (BookmarkletImport, Comment, CookLog, Food, ImportLog, Ingredient, InviteLink,
Keyword, MealPlan, MealType, NutritionInformation, Property, PropertyType, Keyword, MealPlan, MealType, NutritionInformation, Property, PropertyType,
Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchPreference, ShareLink, Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchPreference, ShareLink,
ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog, Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog,
TelegramBot, Unit, UnitConversion, UserFile, UserPreference, UserSpace, TelegramBot, Unit, UnitConversion, UserFile, UserPreference, UserSpace,
ViewLog, ConnectorConfig) ViewLog, ConnectorConfig)
@@ -369,13 +369,6 @@ class ShoppingListEntryAdmin(admin.ModelAdmin):
admin.site.register(ShoppingListEntry, ShoppingListEntryAdmin) admin.site.register(ShoppingListEntry, ShoppingListEntryAdmin)
# class ShoppingListAdmin(admin.ModelAdmin):
# list_display = ('id', 'created_by', 'created_at')
# admin.site.register(ShoppingList, ShoppingListAdmin)
class ShareLinkAdmin(admin.ModelAdmin): class ShareLinkAdmin(admin.ModelAdmin):
list_display = ('recipe', 'created_by', 'uuid', 'created_at',) list_display = ('recipe', 'created_by', 'uuid', 'created_at',)

View File

@@ -160,6 +160,7 @@ class StorageForm(forms.ModelForm):
help_texts = {'url': _('Leave empty for dropbox and enter only base url for nextcloud (<code>/remote.php/webdav/</code> is added automatically)'), } help_texts = {'url': _('Leave empty for dropbox and enter only base url for nextcloud (<code>/remote.php/webdav/</code> is added automatically)'), }
class ConnectorConfigForm(forms.ModelForm): class ConnectorConfigForm(forms.ModelForm):
enabled = forms.BooleanField( enabled = forms.BooleanField(
help_text="Is the connector enabled", help_text="Is the connector enabled",
@@ -205,24 +206,6 @@ class ConnectorConfigForm(forms.ModelForm):
} }
# TODO: Deprecate
# class RecipeBookEntryForm(forms.ModelForm):
# prefix = 'bookmark'
# def __init__(self, *args, **kwargs):
# space = kwargs.pop('space')
# super().__init__(*args, **kwargs)
# self.fields['book'].queryset = RecipeBook.objects.filter(space=space).all()
# class Meta:
# model = RecipeBookEntry
# fields = ('book',)
# field_classes = {
# 'book': SafeModelChoiceField,
# }
class SyncForm(forms.ModelForm): class SyncForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@@ -239,7 +222,6 @@ class SyncForm(forms.ModelForm):
labels = {'storage': _('Storage'), 'path': _('Path'), 'active': _('Active')} labels = {'storage': _('Storage'), 'path': _('Path'), 'active': _('Active')}
# TODO deprecate
class BatchEditForm(forms.Form): class BatchEditForm(forms.Form):
search = forms.CharField(label=_('Search String')) search = forms.CharField(label=_('Search String'))
keywords = forms.ModelMultipleChoiceField(queryset=Keyword.objects.none(), required=False, widget=MultiSelectWidget) keywords = forms.ModelMultipleChoiceField(queryset=Keyword.objects.none(), required=False, widget=MultiSelectWidget)
@@ -373,73 +355,3 @@ class SearchPreferenceForm(forms.ModelForm):
'search': SelectWidget, 'unaccent': MultiSelectWidget, 'icontains': MultiSelectWidget, 'istartswith': MultiSelectWidget, 'trigram': MultiSelectWidget, 'fulltext': 'search': SelectWidget, 'unaccent': MultiSelectWidget, 'icontains': MultiSelectWidget, 'istartswith': MultiSelectWidget, 'trigram': MultiSelectWidget, 'fulltext':
MultiSelectWidget, MultiSelectWidget,
} }
# class ShoppingPreferenceForm(forms.ModelForm):
# prefix = 'shopping'
# class Meta:
# model = UserPreference
# fields = (
# 'shopping_share', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'mealplan_autoexclude_onhand',
# 'mealplan_autoinclude_related', 'shopping_add_onhand', 'default_delay', 'filter_to_supermarket', 'shopping_recent_days', 'csv_delim', 'csv_prefix'
# )
# help_texts = {
# 'shopping_share': _('Users will see all items you add to your shopping list. They must add you to see items on their list.'),
# 'shopping_auto_sync': _(
# 'Setting to 0 will disable auto sync. When viewing a shopping list the list is updated every set seconds to sync changes someone else might have made. Useful when shopping with multiple people but might use a little bit '
# 'of mobile data. If lower than instance limit it is reset when saving.'
# ),
# 'mealplan_autoadd_shopping': _('Automatically add meal plan ingredients to shopping list.'),
# 'mealplan_autoinclude_related': _('When adding a meal plan to the shopping list (manually or automatically), include all related recipes.'),
# 'mealplan_autoexclude_onhand': _('When adding a meal plan to the shopping list (manually or automatically), exclude ingredients that are on hand.'),
# 'default_delay': _('Default number of hours to delay a shopping list entry.'),
# 'filter_to_supermarket': _('Filter shopping list to only include supermarket categories.'),
# 'shopping_recent_days': _('Days of recent shopping list entries to display.'),
# 'shopping_add_onhand': _("Mark food 'On Hand' when checked off shopping list."),
# 'csv_delim': _('Delimiter to use for CSV exports.'),
# 'csv_prefix': _('Prefix to add when copying list to the clipboard.'),
# }
# labels = {
# 'shopping_share': _('Share Shopping List'),
# 'shopping_auto_sync': _('Autosync'),
# 'mealplan_autoadd_shopping': _('Auto Add Meal Plan'),
# 'mealplan_autoexclude_onhand': _('Exclude On Hand'),
# 'mealplan_autoinclude_related': _('Include Related'),
# 'default_delay': _('Default Delay Hours'),
# 'filter_to_supermarket': _('Filter to Supermarket'),
# 'shopping_recent_days': _('Recent Days'),
# 'csv_delim': _('CSV Delimiter'),
# "csv_prefix_label": _("List Prefix"),
# 'shopping_add_onhand': _("Auto On Hand"),
# }
# widgets = {
# 'shopping_share': MultiSelectWidget
# }
# class SpacePreferenceForm(forms.ModelForm):
# prefix = 'space'
# reset_food_inherit = forms.BooleanField(label=_("Reset Food Inheritance"), initial=False, required=False,
# help_text=_("Reset all food to inherit the fields configured."))
# def __init__(self, *args, **kwargs):
# super().__init__(*args, **kwargs) # populates the post
# self.fields['food_inherit'].queryset = Food.inheritable_fields
# class Meta:
# model = Space
# fields = ('food_inherit', 'reset_food_inherit',)
# help_texts = {
# 'food_inherit': _('Fields on food that should be inherited by default.'),
# 'use_plural': _('Use the plural form for units and food inside this space.'),
# }
# widgets = {
# 'food_inherit': MultiSelectWidget
# }

View File

@@ -1,6 +1,4 @@
import cookbook.helper.dal
from cookbook.helper.AllAuthCustomAdapter import AllAuthCustomAdapter from cookbook.helper.AllAuthCustomAdapter import AllAuthCustomAdapter
__all__ = [ __all__ = [
'dal',
] ]

View File

@@ -1,34 +0,0 @@
from cookbook.models import Food, Keyword, Recipe, Unit
from dal import autocomplete
class BaseAutocomplete(autocomplete.Select2QuerySetView):
model = None
def get_queryset(self):
if not self.request.user.is_authenticated:
return self.model.objects.none()
qs = self.model.objects.filter(space=self.request.space).all()
if self.q:
qs = qs.filter(name__icontains=self.q)
return qs
class KeywordAutocomplete(BaseAutocomplete):
model = Keyword
class IngredientsAutocomplete(BaseAutocomplete):
model = Food
class RecipeAutocomplete(BaseAutocomplete):
model = Recipe
class UnitAutocomplete(BaseAutocomplete):
model = Unit

View File

@@ -75,7 +75,7 @@ def is_object_owner(user, obj):
if not user.is_authenticated: if not user.is_authenticated:
return False return False
try: try:
return obj.get_owner() == user return obj.get_owner() == 'orphan' or obj.get_owner() == user
except Exception: except Exception:
return False return False

View File

@@ -1,9 +1,8 @@
from datetime import timedelta
from decimal import Decimal from decimal import Decimal
from django.db.models import F, OuterRef, Q, Subquery, Value from django.db.models import F, OuterRef, Q, Subquery, Value
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.utils import timezone
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from cookbook.models import (Ingredient, MealPlan, Recipe, ShoppingListEntry, ShoppingListRecipe, from cookbook.models import (Ingredient, MealPlan, Recipe, ShoppingListEntry, ShoppingListRecipe,
@@ -76,10 +75,8 @@ class RecipeShoppingEditor():
@staticmethod @staticmethod
def get_shopping_list_recipe(id, user, space): def get_shopping_list_recipe(id, user, space):
return ShoppingListRecipe.objects.filter(id=id).filter(Q(shoppinglist__space=space) | Q(entries__space=space)).filter( return ShoppingListRecipe.objects.filter(id=id).filter(entries__space=space).filter(
Q(shoppinglist__created_by=user) Q(entries__created_by=user)
| Q(shoppinglist__shared=user)
| Q(entries__created_by=user)
| Q(entries__created_by__in=list(user.get_shopping_share())) | Q(entries__created_by__in=list(user.get_shopping_share()))
).prefetch_related('entries').first() ).prefetch_related('entries').first()

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-11 15:09+0200\n" "POT-Creation-Date: 2021-04-11 15:09+0200\n"
"PO-Revision-Date: 2022-04-17 00:31+0000\n" "PO-Revision-Date: 2024-02-27 12:19+0000\n"
"Last-Translator: Oskar Stenberg <01ste02@gmail.com>\n" "Last-Translator: Lukas Åteg <lukas@ategsolutions.se>\n"
"Language-Team: Swedish <http://translate.tandoor.dev/projects/tandoor/" "Language-Team: Swedish <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/sv/>\n" "recipes-backend/sv/>\n"
"Language: sv\n" "Language: sv\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.10.1\n" "X-Generator: Weblate 4.15\n"
#: .\cookbook\filters.py:23 .\cookbook\templates\base.html:91 #: .\cookbook\filters.py:23 .\cookbook\templates\base.html:91
#: .\cookbook\templates\forms\edit_internal_recipe.html:219 #: .\cookbook\templates\forms\edit_internal_recipe.html:219
@@ -1812,7 +1812,7 @@ msgstr "Google ld+json info"
#: .\cookbook\templates\url_import.html:268 #: .\cookbook\templates\url_import.html:268
msgid "GitHub Issues" msgid "GitHub Issues"
msgstr "GitHub Issues" msgstr "GitHub Problem"
#: .\cookbook\templates\url_import.html:270 #: .\cookbook\templates\url_import.html:270
msgid "Recipe Markup Specification" msgid "Recipe Markup Specification"
@@ -1852,7 +1852,7 @@ msgstr "Kunde inte tolka korrekt..."
msgid "Batch edit done. %(count)d recipe was updated." msgid "Batch edit done. %(count)d recipe was updated."
msgid_plural "Batch edit done. %(count)d Recipes where updated." msgid_plural "Batch edit done. %(count)d Recipes where updated."
msgstr[0] "Batchredigering klar. %(count)d recept uppdaterades." msgstr[0] "Batchredigering klar. %(count)d recept uppdaterades."
msgstr[1] "Batchredigering klar. %(count)d recept uppdaterades." msgstr[1] "Batchredigering klar. %(count)d recepten uppdaterades."
#: .\cookbook\views\delete.py:72 #: .\cookbook\views\delete.py:72
msgid "Monitor" msgid "Monitor"

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.2.10 on 2024-02-19 20:21
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
(
"cookbook",
"0213_remove_property_property_unique_import_food_per_space_and_more",
),
]
operations = [
migrations.DeleteModel(
name="ShoppingList",
),
]

View File

@@ -369,9 +369,8 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
Storage.objects.filter(space=self).delete() Storage.objects.filter(space=self).delete()
ConnectorConfig.objects.filter(space=self).delete() ConnectorConfig.objects.filter(space=self).delete()
ShoppingListEntry.objects.filter(shoppinglist__space=self).delete() ShoppingListEntry.objects.filter(space=self).delete()
ShoppingListRecipe.objects.filter(shoppinglist__space=self).delete() ShoppingListRecipe.objects.filter(recipe__space=self).delete()
ShoppingList.objects.filter(space=self).delete()
SupermarketCategoryRelation.objects.filter(supermarket__space=self).delete() SupermarketCategoryRelation.objects.filter(supermarket__space=self).delete()
SupermarketCategory.objects.filter(space=self).delete() SupermarketCategory.objects.filter(space=self).delete()
@@ -1195,7 +1194,10 @@ class ShoppingListRecipe(ExportModelOperationsMixin('shopping_list_recipe'), mod
def get_owner(self): def get_owner(self):
try: try:
return getattr(self.entries.first(), 'created_by', None) or getattr(self.shoppinglist_set.first(), 'created_by', None) if not self.entries.exists():
return 'orphan'
else:
return getattr(self.entries.first(), 'created_by', None)
except AttributeError: except AttributeError:
return None return None
@@ -1218,53 +1220,19 @@ class ShoppingListEntry(ExportModelOperationsMixin('shopping_list_entry'), model
space = models.ForeignKey(Space, on_delete=models.CASCADE) space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space') objects = ScopedManager(space='space')
@staticmethod
def get_space_key():
return 'shoppinglist', 'space'
def get_space(self):
return self.shoppinglist_set.first().space
def __str__(self): def __str__(self):
return f'Shopping list entry {self.id}' return f'Shopping list entry {self.id}'
def get_shared(self): def get_shared(self):
try: return self.created_by.userpreference.shopping_share.all()
return self.shoppinglist_set.first().shared.all()
except AttributeError:
return self.created_by.userpreference.shopping_share.all()
def get_owner(self): def get_owner(self):
try: try:
return self.created_by or self.shoppinglist_set.first().created_by return self.created_by
except AttributeError: except AttributeError:
return None return None
class ShoppingList(ExportModelOperationsMixin('shopping_list'), models.Model, PermissionModelMixin):
uuid = models.UUIDField(default=uuid.uuid4)
note = models.TextField(blank=True, null=True)
recipes = models.ManyToManyField(ShoppingListRecipe, blank=True)
entries = models.ManyToManyField(ShoppingListEntry, blank=True)
shared = models.ManyToManyField(User, blank=True, related_name='list_share')
supermarket = models.ForeignKey(Supermarket, null=True, blank=True, on_delete=models.SET_NULL)
finished = models.BooleanField(default=False)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
return f'Shopping list {self.id}'
def get_shared(self):
try:
return self.shared.all() or self.created_by.userpreference.shopping_share.all()
except AttributeError:
return []
class ShareLink(ExportModelOperationsMixin('share_link'), models.Model, PermissionModelMixin): class ShareLink(ExportModelOperationsMixin('share_link'), models.Model, PermissionModelMixin):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE) recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
uuid = models.UUIDField(default=uuid.uuid4) uuid = models.UUIDField(default=uuid.uuid4)

View File

@@ -1,4 +1,3 @@
import traceback
import uuid import uuid
from datetime import datetime, timedelta from datetime import datetime, timedelta
from decimal import Decimal from decimal import Decimal
@@ -31,7 +30,7 @@ from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, Cu
ExportLog, Food, FoodInheritField, ImportLog, Ingredient, InviteLink, ExportLog, Food, FoodInheritField, ImportLog, Ingredient, InviteLink,
Keyword, MealPlan, MealType, NutritionInformation, Property, Keyword, MealPlan, MealType, NutritionInformation, Property,
PropertyType, Recipe, RecipeBook, RecipeBookEntry, RecipeImport, PropertyType, Recipe, RecipeBook, RecipeBookEntry, RecipeImport,
ShareLink, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space, ShareLink, ShoppingListEntry, ShoppingListRecipe, Space,
Step, Storage, Supermarket, SupermarketCategory, Step, Storage, Supermarket, SupermarketCategory,
SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion, SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion,
UserFile, UserPreference, UserSpace, ViewLog, ConnectorConfig) UserFile, UserPreference, UserSpace, ViewLog, ConnectorConfig)
@@ -82,14 +81,12 @@ class ExtendedRecipeMixin(serializers.ModelSerializer):
class OpenDataModelMixin(serializers.ModelSerializer): class OpenDataModelMixin(serializers.ModelSerializer):
def create(self, validated_data): def create(self, validated_data):
if 'open_data_slug' in validated_data and validated_data['open_data_slug'] is not None and validated_data[ if 'open_data_slug' in validated_data and validated_data['open_data_slug'] is not None and validated_data['open_data_slug'].strip() == '':
'open_data_slug'].strip() == '':
validated_data['open_data_slug'] = None validated_data['open_data_slug'] = None
return super().create(validated_data) return super().create(validated_data)
def update(self, instance, validated_data): def update(self, instance, validated_data):
if 'open_data_slug' in validated_data and validated_data['open_data_slug'] is not None and validated_data[ if 'open_data_slug' in validated_data and validated_data['open_data_slug'] is not None and validated_data['open_data_slug'].strip() == '':
'open_data_slug'].strip() == '':
validated_data['open_data_slug'] = None validated_data['open_data_slug'] = None
return super().update(instance, validated_data) return super().update(instance, validated_data)
@@ -336,8 +333,7 @@ class UserSpaceSerializer(WritableNestedModelSerializer):
class Meta: class Meta:
model = UserSpace model = UserSpace
fields = ( fields = ('id', 'user', 'space', 'groups', 'active', 'internal_note', 'invite_link', 'created_at', 'updated_at',)
'id', 'user', 'space', 'groups', 'active', 'internal_note', 'invite_link', 'created_at', 'updated_at',)
read_only_fields = ('id', 'invite_link', 'created_at', 'updated_at', 'space') read_only_fields = ('id', 'invite_link', 'created_at', 'updated_at', 'space')
@@ -877,8 +873,7 @@ class UnitConversionSerializer(WritableNestedModelSerializer, OpenDataModelMixin
class Meta: class Meta:
model = UnitConversion model = UnitConversion
fields = ( fields = ('id', 'name', 'base_amount', 'base_unit', 'converted_amount', 'converted_unit', 'food', 'open_data_slug')
'id', 'name', 'base_amount', 'base_unit', 'converted_amount', 'converted_unit', 'food', 'open_data_slug')
class NutritionInformationSerializer(serializers.ModelSerializer): class NutritionInformationSerializer(serializers.ModelSerializer):
@@ -1198,37 +1193,6 @@ class ShoppingListEntryCheckedSerializer(serializers.ModelSerializer):
fields = ('id', 'checked') fields = ('id', 'checked')
# TODO deprecate
class ShoppingListSerializer(WritableNestedModelSerializer):
recipes = ShoppingListRecipeSerializer(many=True, allow_null=True)
entries = ShoppingListEntrySerializer(many=True, allow_null=True)
shared = UserSerializer(many=True)
supermarket = SupermarketSerializer(allow_null=True)
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
validated_data['created_by'] = self.context['request'].user
return super().create(validated_data)
class Meta:
model = ShoppingList
fields = (
'id', 'uuid', 'note', 'recipes', 'entries',
'shared', 'finished', 'supermarket', 'created_by', 'created_at'
)
read_only_fields = ('id', 'created_by',)
# TODO deprecate
class ShoppingListAutoSyncSerializer(WritableNestedModelSerializer):
entries = ShoppingListEntryCheckedSerializer(many=True, allow_null=True)
class Meta:
model = ShoppingList
fields = ('id', 'entries',)
read_only_fields = ('id',)
class ShareLinkSerializer(SpacedModelSerializer): class ShareLinkSerializer(SpacedModelSerializer):
class Meta: class Meta:
model = ShareLink model = ShareLink

View File

@@ -114,7 +114,7 @@
class="fas fa-fw fa-calendar"></i> {% trans 'Meal-Plan' %}</a> class="fas fa-fw fa-calendar"></i> {% trans 'Meal-Plan' %}</a>
</li> </li>
<li class="nav-item {% if request.resolver_match.url_name in 'list_shopping_list,view_shopping' %}active{% endif %}"> <li class="nav-item {% if request.resolver_match.url_name in 'list_shopping_list,view_shopping' %}active{% endif %}">
<a class="nav-link" href="{% url 'list_shopping_list' %}"><i <a class="nav-link" href="{% url 'view_shopping' %}"><i
class="fas fa-fw fa-shopping-cart"></i> {% trans 'Shopping' %}</a> class="fas fa-fw fa-shopping-cart"></i> {% trans 'Shopping' %}</a>
</li> </li>
<li class="nav-item {% if request.resolver_match.url_name in 'view_books' %}active{% endif %}"> <li class="nav-item {% if request.resolver_match.url_name in 'view_books' %}active{% endif %}">

View File

@@ -1,32 +0,0 @@
{% extends "base.html" %}
{% load render_bundle from webpack_loader %}
{% load static %}
{% load i18n %}
{% load l10n %}
{% block title %}{% trans 'Profile' %}{% endblock %}
{% block content %}
<div id="app" >
<profile-view></profile-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.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}'
</script>
{% render_bundle 'profile_view' %}
{% endblock %}

View File

@@ -1,36 +0,0 @@
{% extends "base.html" %}
{% load render_bundle from webpack_loader %}
{% load static %}
{% load i18n %}
{% load l10n %}
{% block title %}{% trans 'Supermarket' %}{% endblock %}
{% block extra_head %}
<link rel="stylesheet" href="{% static 'css/vue-multiselect-bs4.min.css' %}">
{% endblock %}
{% block content %}
<div id="app" >
<supermarket-view></supermarket-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.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}'
</script>
{% render_bundle 'supermarket_view' %}
{% endblock %}

View File

@@ -1,135 +0,0 @@
import json
import pytest
from django.contrib import auth
from django.urls import reverse
from django_scopes import scopes_disabled
from cookbook.models import ShoppingList
LIST_URL = 'api:shoppinglist-list'
DETAIL_URL = 'api:shoppinglist-detail'
@pytest.fixture()
def obj_1(space_1, u1_s1):
return ShoppingList.objects.create(created_by=auth.get_user(u1_s1), space=space_1, )
@pytest.fixture
def obj_2(space_1, u1_s1):
return ShoppingList.objects.create(created_by=auth.get_user(u1_s1), space=space_1, )
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 200],
['u1_s1', 200],
['a1_s1', 200],
])
def test_list_permission(arg, request):
c = request.getfixturevalue(arg[0])
assert c.get(reverse(LIST_URL)).status_code == arg[1]
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
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)) == 0
def test_share(obj_1, u1_s1, u2_s1, u1_s2):
assert u1_s1.get(reverse(DETAIL_URL, args={obj_1.id})).status_code == 200
assert u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).status_code == 404
assert u1_s2.get(reverse(DETAIL_URL, args={obj_1.id})).status_code == 404
obj_1.shared.add(auth.get_user(u2_s1))
obj_1.shared.add(auth.get_user(u1_s2))
assert u1_s1.get(reverse(DETAIL_URL, args={obj_1.id})).status_code == 200
assert u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).status_code == 200
assert u1_s2.get(reverse(DETAIL_URL, args={obj_1.id})).status_code == 404
def test_new_share(request, obj_1, u1_s1, u2_s1, u1_s2):
assert u1_s1.get(reverse(DETAIL_URL, args={obj_1.id})).status_code == 200
assert u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).status_code == 404
assert u1_s2.get(reverse(DETAIL_URL, args={obj_1.id})).status_code == 404
with scopes_disabled():
user = auth.get_user(u1_s1)
user.userpreference.shopping_share.add(auth.get_user(u2_s1))
user.userpreference.save()
assert u1_s1.get(reverse(DETAIL_URL, args={obj_1.id})).status_code == 200
assert u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).status_code == 200
assert u1_s2.get(reverse(DETAIL_URL, args={obj_1.id})).status_code == 404
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 404],
['u1_s1', 200],
['a1_s1', 404],
['g1_s2', 404],
['u1_s2', 404],
['a1_s2', 404],
])
def test_update(arg, request, obj_1):
c = request.getfixturevalue(arg[0])
r = c.patch(
reverse(
DETAIL_URL,
args={obj_1.id}
),
{'note': 'new'},
content_type='application/json'
)
assert r.status_code == arg[1]
if r.status_code == 200:
response = json.loads(r.content)
assert response['note'] == 'new'
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 201],
['u1_s1', 201],
['a1_s1', 201],
])
def test_add(arg, request):
c = request.getfixturevalue(arg[0])
r = c.post(
reverse(LIST_URL),
{'note': 'test', 'recipes': [], 'shared': [], 'entries': [], 'supermarket': None},
content_type='application/json'
)
response = json.loads(r.content)
print(r.content)
assert r.status_code == arg[1]
if r.status_code == 201:
assert response['note'] == 'test'
def test_delete(u1_s1, u1_s2, obj_1):
r = u1_s2.delete(
reverse(
DETAIL_URL,
args={obj_1.id}
)
)
assert r.status_code == 404
r = u1_s1.delete(
reverse(
DETAIL_URL,
args={obj_1.id}
)
)
assert r.status_code == 204

View File

@@ -1,148 +0,0 @@
import json
import pytest
from django.contrib import auth
from django.urls import reverse
from django_scopes import scopes_disabled
from cookbook.models import Food, ShoppingList, ShoppingListEntry
LIST_URL = 'api:shoppinglistentry-list'
DETAIL_URL = 'api:shoppinglistentry-detail'
@pytest.fixture()
def obj_1(space_1, u1_s1):
e = ShoppingListEntry.objects.create(created_by=auth.get_user(u1_s1),
food=Food.objects.get_or_create(name='test 1', space=space_1)[0],
space=space_1)
s = ShoppingList.objects.create(created_by=auth.get_user(u1_s1), space=space_1, )
s.entries.add(e)
return e
@pytest.fixture
def obj_2(space_1, u1_s1):
e = ShoppingListEntry.objects.create(created_by=auth.get_user(u1_s1),
food=Food.objects.get_or_create(name='test 2', space=space_1)[0],
space=space_1)
s = ShoppingList.objects.create(created_by=auth.get_user(u1_s1), space=space_1, )
s.entries.add(e)
return e
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 200],
['u1_s1', 200],
['a1_s1', 200],
])
def test_list_permission(arg, request):
c = request.getfixturevalue(arg[0])
assert c.get(reverse(LIST_URL)).status_code == arg[1]
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
with scopes_disabled():
s = ShoppingList.objects.first()
e = ShoppingListEntry.objects.first()
s.space = space_2
e.space = space_2
s.save()
e.save()
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 1
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 404],
['u1_s1', 200],
['a1_s1', 404],
['g1_s2', 404],
['u1_s2', 404],
['a1_s2', 404],
])
def test_update(arg, request, obj_1):
c = request.getfixturevalue(arg[0])
r = c.patch(
reverse(
DETAIL_URL,
args={obj_1.id}
),
{'amount': 2},
content_type='application/json'
)
assert r.status_code == arg[1]
if r.status_code == 200:
response = json.loads(r.content)
assert response['amount'] == 2
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 200],
['a1_s1', 200],
['g1_s2', 403],
['u1_s2', 200],
['a1_s2', 200],
])
def test_bulk_update(arg, request, obj_1, obj_2):
c = request.getfixturevalue(arg[0])
r = c.post(
reverse(LIST_URL, ) + 'bulk/',
{'ids': [obj_1.id, obj_2.id], 'checked': True},
content_type='application/json'
)
assert r.status_code == arg[1]
assert r
if r.status_code == 200:
obj_1.refresh_from_db()
assert obj_1.checked == (arg[0] == 'u1_s1')
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 201],
['u1_s1', 201],
['a1_s1', 201],
])
def test_add(arg, request, obj_1):
c = request.getfixturevalue(arg[0])
r = c.post(
reverse(LIST_URL),
{'food': {
'id': obj_1.food.__dict__['id'],
'name': obj_1.food.__dict__['name'],
}, 'amount': 1},
content_type='application/json'
)
response = json.loads(r.content)
print(r.content)
assert r.status_code == arg[1]
if r.status_code == 201:
assert response['food']['id'] == obj_1.food.pk
def test_delete(u1_s1, u1_s2, obj_1):
r = u1_s2.delete(
reverse(
DETAIL_URL,
args={obj_1.id}
)
)
assert r.status_code == 404
r = u1_s1.delete(
reverse(
DETAIL_URL,
args={obj_1.id}
)
)
assert r.status_code == 204

View File

@@ -214,7 +214,7 @@ def test_completed(sle, u1_s1):
def test_recent(sle, u1_s1): def test_recent(sle, u1_s1):
user = auth.get_user(u1_s1) user = auth.get_user(u1_s1)
user.userpreference.shopping_recent_days = 7 # hardcoded API limit 14 days user.userpreference.shopping_recent_days = 7 # hardcoded API limit 14 days
user.userpreference.save() user.userpreference.save()
today_start = timezone.now().replace(hour=0, minute=0, second=0) today_start = timezone.now().replace(hour=0, minute=0, second=0)

View File

@@ -3,9 +3,8 @@ import json
import pytest import pytest
from django.contrib import auth from django.contrib import auth
from django.urls import reverse from django.urls import reverse
from django_scopes import scopes_disabled
from cookbook.models import ShoppingList, ShoppingListRecipe from cookbook.models import ShoppingListEntry, ShoppingListRecipe
LIST_URL = 'api:shoppinglistrecipe-list' LIST_URL = 'api:shoppinglistrecipe-list'
DETAIL_URL = 'api:shoppinglistrecipe-detail' DETAIL_URL = 'api:shoppinglistrecipe-detail'
@@ -14,81 +13,31 @@ DETAIL_URL = 'api:shoppinglistrecipe-detail'
@pytest.fixture() @pytest.fixture()
def obj_1(space_1, u1_s1, recipe_1_s1): def obj_1(space_1, u1_s1, recipe_1_s1):
r = ShoppingListRecipe.objects.create(recipe=recipe_1_s1, servings=1) r = ShoppingListRecipe.objects.create(recipe=recipe_1_s1, servings=1)
s = ShoppingList.objects.create(created_by=auth.get_user(u1_s1), space=space_1, ) for ing in r.recipe.steps.first().ingredients.all():
s.recipes.add(r) ShoppingListEntry.objects.create(list_recipe=r, ingredient=ing, food=ing.food, unit=ing.unit, amount=ing.amount, created_by=auth.get_user(u1_s1), space=space_1)
return r return r
@pytest.fixture @pytest.mark.parametrize("arg", [['a_u', 403], ['g1_s1', 200], ['u1_s1', 200], ['a1_s1', 200], ])
def obj_2(space_1, u1_s1, recipe_1_s1):
r = ShoppingListRecipe.objects.create(recipe=recipe_1_s1, servings=1)
s = ShoppingList.objects.create(created_by=auth.get_user(u1_s1), space=space_1, )
s.recipes.add(r)
return r
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 200],
['u1_s1', 200],
['a1_s1', 200],
])
def test_list_permission(arg, request): def test_list_permission(arg, request):
c = request.getfixturevalue(arg[0]) c = request.getfixturevalue(arg[0])
assert c.get(reverse(LIST_URL)).status_code == arg[1] assert c.get(reverse(LIST_URL)).status_code == arg[1]
def test_list_space(obj_1, obj_2, u1_s1, u1_s2, space_2): @pytest.mark.parametrize("arg", [['a_u', 403], ['g1_s1', 404], ['u1_s1', 200], ['a1_s1', 404], ['g1_s2', 404], ['u1_s2', 404], ['a1_s2', 404], ])
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 2
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0
with scopes_disabled():
s = ShoppingList.objects.first()
s.space = space_2
s.save()
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 1
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 404],
['u1_s1', 200],
['a1_s1', 404],
['g1_s2', 404],
['u1_s2', 404],
['a1_s2', 404],
])
def test_update(arg, request, obj_1): def test_update(arg, request, obj_1):
c = request.getfixturevalue(arg[0]) c = request.getfixturevalue(arg[0])
r = c.patch( r = c.patch(reverse(DETAIL_URL, args={obj_1.id}), {'servings': 2}, content_type='application/json')
reverse(
DETAIL_URL,
args={obj_1.id}
),
{'servings': 2},
content_type='application/json'
)
assert r.status_code == arg[1] assert r.status_code == arg[1]
if r.status_code == 200: if r.status_code == 200:
response = json.loads(r.content) response = json.loads(r.content)
assert response['servings'] == 2 assert response['servings'] == 2
@pytest.mark.parametrize("arg", [ @pytest.mark.parametrize("arg", [['a_u', 403], ['g1_s1', 201], ['u1_s1', 201], ['a1_s1', 201], ])
['a_u', 403],
['g1_s1', 201],
['u1_s1', 201],
['a1_s1', 201],
])
def test_add(arg, request, obj_1, recipe_1_s1): def test_add(arg, request, obj_1, recipe_1_s1):
c = request.getfixturevalue(arg[0]) c = request.getfixturevalue(arg[0])
r = c.post( r = c.post(reverse(LIST_URL), {'recipe': recipe_1_s1.pk, 'servings': 1}, content_type='application/json')
reverse(LIST_URL),
{'recipe': recipe_1_s1.pk, 'servings': 1},
content_type='application/json'
)
response = json.loads(r.content) response = json.loads(r.content)
print(r.content) print(r.content)
assert r.status_code == arg[1] assert r.status_code == arg[1]
@@ -97,19 +46,9 @@ def test_add(arg, request, obj_1, recipe_1_s1):
def test_delete(u1_s1, u1_s2, obj_1): def test_delete(u1_s1, u1_s2, obj_1):
r = u1_s2.delete( r = u1_s2.delete(reverse(DETAIL_URL, args={obj_1.id}))
reverse(
DETAIL_URL,
args={obj_1.id}
)
)
assert r.status_code == 404 assert r.status_code == 404
r = u1_s1.delete( r = u1_s1.delete(reverse(DETAIL_URL, args={obj_1.id}))
reverse(
DETAIL_URL,
args={obj_1.id}
)
)
assert r.status_code == 204 assert r.status_code == 204

View File

@@ -6,7 +6,7 @@ from django.contrib import auth
from django.urls import reverse from django.urls import reverse
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
from cookbook.models import Food, Ingredient, ShoppingList, ShoppingListEntry, Unit from cookbook.models import Food, Ingredient, ShoppingListEntry, Unit
LIST_URL = 'api:unit-list' LIST_URL = 'api:unit-list'
DETAIL_URL = 'api:unit-detail' DETAIL_URL = 'api:unit-detail'
@@ -50,8 +50,6 @@ def ing_3_s2(obj_3, space_2, u2_s2):
@pytest.fixture() @pytest.fixture()
def sle_1_s1(obj_1, u1_s1, space_1): def sle_1_s1(obj_1, u1_s1, space_1):
e = ShoppingListEntry.objects.create(unit=obj_1, food=random_food(space_1, u1_s1), created_by=auth.get_user(u1_s1), space=space_1,) e = ShoppingListEntry.objects.create(unit=obj_1, food=random_food(space_1, u1_s1), created_by=auth.get_user(u1_s1), space=space_1,)
s = ShoppingList.objects.create(created_by=auth.get_user(u1_s1), space=space_1, )
s.entries.add(e)
return e return e
@@ -63,8 +61,6 @@ def sle_2_s1(obj_2, u1_s1, space_1):
@pytest.fixture() @pytest.fixture()
def sle_3_s2(obj_3, u2_s2, space_2): def sle_3_s2(obj_3, u2_s2, space_2):
e = ShoppingListEntry.objects.create(unit=obj_3, food=random_food(space_2, u2_s2), created_by=auth.get_user(u2_s2), space=space_2) e = ShoppingListEntry.objects.create(unit=obj_3, food=random_food(space_2, u2_s2), created_by=auth.get_user(u2_s2), space=space_2)
s = ShoppingList.objects.create(created_by=auth.get_user(u2_s2), space=space_2)
s.entries.add(e)
return e return e

View File

@@ -5,20 +5,22 @@ from django.views.generic import TemplateView
from rest_framework import permissions, routers from rest_framework import permissions, routers
from rest_framework.schemas import get_schema_view from rest_framework.schemas import get_schema_view
from cookbook.helper import dal
from cookbook.version_info import TANDOOR_VERSION from cookbook.version_info import TANDOOR_VERSION
from recipes.settings import DEBUG, PLUGINS from recipes.settings import DEBUG, PLUGINS
from .models import (Automation, Comment, CustomFilter, Food, InviteLink, Keyword, PropertyType, from .models import (Automation, Comment, CustomFilter, Food, InviteLink, Keyword, PropertyType,
Recipe, RecipeBook, RecipeBookEntry, RecipeImport, ShoppingList, Space, Step, Recipe, RecipeBook, RecipeBookEntry, RecipeImport, Space, Step,
Storage, Supermarket, SupermarketCategory, Sync, SyncLog, Unit, UnitConversion, Storage, Supermarket, SupermarketCategory, Sync, SyncLog, Unit, UnitConversion,
UserFile, UserSpace, get_model_name, ConnectorConfig) UserFile, UserSpace, get_model_name, ConnectorConfig)
from .views import api, data, delete, edit, import_export, lists, new, telegram, views from .views import api, data, delete, edit, import_export, lists, new, telegram, views
from .views.api import CustomAuthToken, ImportOpenData from .views.api import CustomAuthToken, ImportOpenData
# extend DRF default router class to allow including additional routers # extend DRF default router class to allow including additional routers
class DefaultRouter(routers.DefaultRouter): class DefaultRouter(routers.DefaultRouter):
def extend(self, r): def extend(self, r):
self.registry.extend(r.registry) self.registry.extend(r.registry)
@@ -45,7 +47,6 @@ router.register(r'recipe-book-entry', api.RecipeBookEntryViewSet)
router.register(r'unit-conversion', api.UnitConversionViewSet) router.register(r'unit-conversion', api.UnitConversionViewSet)
router.register(r'food-property-type', api.PropertyTypeViewSet) # TODO rename + regenerate router.register(r'food-property-type', api.PropertyTypeViewSet) # TODO rename + regenerate
router.register(r'food-property', api.PropertyViewSet) router.register(r'food-property', api.PropertyViewSet)
router.register(r'shopping-list', api.ShoppingListViewSet)
router.register(r'shopping-list-entry', api.ShoppingListEntryViewSet) router.register(r'shopping-list-entry', api.ShoppingListEntryViewSet)
router.register(r'shopping-list-recipe', api.ShoppingListRecipeViewSet) router.register(r'shopping-list-recipe', api.ShoppingListRecipeViewSet)
router.register(r'space', api.SpaceViewSet) router.register(r'space', api.SpaceViewSet)
@@ -80,7 +81,6 @@ urlpatterns = [
path('space-overview', views.space_overview, name='view_space_overview'), path('space-overview', views.space_overview, name='view_space_overview'),
path('space-manage/<int:space_id>', views.space_manage, name='view_space_manage'), path('space-manage/<int:space_id>', views.space_manage, name='view_space_manage'),
path('switch-space/<int:space_id>', views.switch_space, name='view_switch_space'), path('switch-space/<int:space_id>', views.switch_space, name='view_switch_space'),
path('profile/<int:user_id>', views.view_profile, name='view_profile'),
path('no-perm', views.no_perm, name='view_no_perm'), path('no-perm', views.no_perm, name='view_no_perm'),
path('invite/<slug:token>', views.invite_link, name='view_invite'), path('invite/<slug:token>', views.invite_link, name='view_invite'),
path('system/', views.system, name='view_system'), path('system/', views.system, name='view_system'),
@@ -89,34 +89,27 @@ urlpatterns = [
path('plan/', views.meal_plan, name='view_plan'), path('plan/', views.meal_plan, name='view_plan'),
path('shopping/', lists.shopping_list, name='view_shopping'), path('shopping/', lists.shopping_list, name='view_shopping'),
path('settings/', views.user_settings, name='view_settings'), path('settings/', views.user_settings, name='view_settings'),
path('settings-shopping/', views.shopping_settings, name='view_shopping_settings'), path('settings-shopping/', views.shopping_settings, name='view_shopping_settings'), # TODO rename to search settings
path('history/', views.history, name='view_history'), path('history/', views.history, name='view_history'),
path('supermarket/', views.supermarket, name='view_supermarket'),
path('ingredient-editor/', views.ingredient_editor, name='view_ingredient_editor'), path('ingredient-editor/', views.ingredient_editor, name='view_ingredient_editor'),
path('property-editor/<int:pk>', views.property_editor, name='view_property_editor'), path('property-editor/<int:pk>', views.property_editor, name='view_property_editor'),
path('abuse/<slug:token>', views.report_share_abuse, name='view_report_share_abuse'), path('abuse/<slug:token>', views.report_share_abuse, name='view_report_share_abuse'),
path('api/import/', api.import_files, name='view_import'), path('api/import/', api.import_files, name='view_import'),
path('import-response/<int:pk>/', import_export.import_response, name='view_import_response'), path('import-response/<int:pk>/', import_export.import_response, name='view_import_response'),
path('export/', import_export.export_recipe, name='view_export'), path('export/', import_export.export_recipe, name='view_export'),
path('export-response/<int:pk>/', import_export.export_response, name='view_export_response'), path('export-response/<int:pk>/', import_export.export_response, name='view_export_response'),
path('export-file/<int:pk>/', import_export.export_file, name='view_export_file'), path('export-file/<int:pk>/', import_export.export_file, name='view_export_file'),
path('view/recipe/<int:pk>', views.recipe_view, name='view_recipe'), path('view/recipe/<int:pk>', views.recipe_view, name='view_recipe'),
path('view/recipe/<int:pk>/<slug:share>', views.recipe_view, name='view_recipe'), path('view/recipe/<int:pk>/<slug:share>', views.recipe_view, name='view_recipe'),
path('new/recipe-import/<int:import_id>/', new.create_new_external_recipe, name='new_recipe_import'), path('new/recipe-import/<int:import_id>/', new.create_new_external_recipe, name='new_recipe_import'),
path('new/share-link/<int:pk>/', new.share_link, name='new_share_link'), path('new/share-link/<int:pk>/', new.share_link, name='new_share_link'),
path('edit/recipe/<int:pk>/', edit.switch_recipe, name='edit_recipe'), path('edit/recipe/<int:pk>/', edit.switch_recipe, name='edit_recipe'),
# for internal use only # for internal use only
path('edit/recipe/internal/<int:pk>/', edit.internal_recipe_update, name='edit_internal_recipe'), path('edit/recipe/internal/<int:pk>/', edit.internal_recipe_update, name='edit_internal_recipe'),
path('edit/recipe/external/<int:pk>/', edit.ExternalRecipeUpdate.as_view(), name='edit_external_recipe'), path('edit/recipe/external/<int:pk>/', edit.ExternalRecipeUpdate.as_view(), name='edit_external_recipe'),
path('edit/recipe/convert/<int:pk>/', edit.convert_recipe, name='edit_convert_recipe'), 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/storage/<int:pk>/', edit.edit_storage, name='edit_storage'),
path('delete/recipe-source/<int:pk>/', delete.delete_recipe_source, name='delete_recipe_source'), path('delete/recipe-source/<int:pk>/', delete.delete_recipe_source, name='delete_recipe_source'),
# TODO move to generic "new" view # TODO move to generic "new" view
@@ -125,7 +118,6 @@ urlpatterns = [
path('data/batch/import', data.batch_import, name='data_batch_import'), path('data/batch/import', data.batch_import, name='data_batch_import'),
path('data/sync/wait', data.sync_wait, name='data_sync_wait'), path('data/sync/wait', data.sync_wait, name='data_sync_wait'),
path('data/import/url', data.import_url, name='data_import_url'), path('data/import/url', data.import_url, name='data_import_url'),
path('api/get_external_file_link/<int:recipe_id>/', api.get_external_file_link, name='api_get_external_file_link'), path('api/get_external_file_link/<int:recipe_id>/', api.get_external_file_link, name='api_get_external_file_link'),
path('api/get_recipe_file/<int:recipe_id>/', api.get_recipe_file, name='api_get_recipe_file'), path('api/get_recipe_file/<int:recipe_id>/', api.get_recipe_file, name='api_get_recipe_file'),
path('api/sync_all/', api.sync_all, name='api_sync'), path('api/sync_all/', api.sync_all, name='api_sync'),
@@ -138,75 +130,45 @@ urlpatterns = [
path('api/reset-food-inheritance/', api.reset_food_inheritance, name='api_reset_food_inheritance'), path('api/reset-food-inheritance/', api.reset_food_inheritance, name='api_reset_food_inheritance'),
path('api/switch-active-space/<int:space_id>/', api.switch_active_space, name='api_switch_active_space'), path('api/switch-active-space/<int:space_id>/', api.switch_active_space, name='api_switch_active_space'),
path('api/download-file/<int:file_id>/', api.download_file, name='api_download_file'), path('api/download-file/<int:file_id>/', api.download_file, name='api_download_file'),
path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'),
# TODO is this deprecated? not yet, some old forms remain, could likely be changed to generic API endpoints
path('dal/food/', dal.IngredientsAutocomplete.as_view(), name='dal_food'), # TODO is this deprecated?
path('dal/unit/', dal.UnitAutocomplete.as_view(), name='dal_unit'), # TODO is this deprecated?
path('telegram/setup/<int:pk>', telegram.setup_bot, name='telegram_setup'), path('telegram/setup/<int:pk>', telegram.setup_bot, name='telegram_setup'),
path('telegram/remove/<int:pk>', telegram.remove_bot, name='telegram_remove'), path('telegram/remove/<int:pk>', telegram.remove_bot, name='telegram_remove'),
path('telegram/hook/<slug:token>/', telegram.hook, name='telegram_hook'), path('telegram/hook/<slug:token>/', telegram.hook, name='telegram_hook'),
path('docs/markdown/', views.markdown_info, name='docs_markdown'), path('docs/markdown/', views.markdown_info, name='docs_markdown'),
path('docs/search/', views.search_info, name='docs_search'), path('docs/search/', views.search_info, name='docs_search'),
path('docs/api/', views.api_info, name='docs_api'), path('docs/api/', views.api_info, name='docs_api'),
path('openapi/', get_schema_view(title="Django Recipes", version=TANDOOR_VERSION, public=True, permission_classes=(permissions.AllowAny, )), name='openapi-schema'),
path('openapi/', get_schema_view(title="Django Recipes", version=TANDOOR_VERSION, public=True,
permission_classes=(permissions.AllowAny,)), name='openapi-schema'),
path('api/', include((router.urls, 'api'))), path('api/', include((router.urls, 'api'))),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')), path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
path('api-token-auth/', CustomAuthToken.as_view()), path('api-token-auth/', CustomAuthToken.as_view()),
path('api-import-open-data/', ImportOpenData.as_view(), name='api_import_open_data'), path('api-import-open-data/', ImportOpenData.as_view(), name='api_import_open_data'),
path('offline/', views.offline, name='view_offline'), path('offline/', views.offline, name='view_offline'),
path('service-worker.js', (TemplateView.as_view(template_name="sw.js", content_type='application/javascript',
path('service-worker.js', (TemplateView.as_view(template_name="sw.js", content_type='application/javascript', )), )), name='service_worker'),
name='service_worker'),
path('manifest.json', views.web_manifest, name='web_manifest'), path('manifest.json', views.web_manifest, name='web_manifest'),
] ]
generic_models = ( generic_models = (
Recipe, RecipeImport, Storage, ConnectorConfig, RecipeBook, SyncLog, Sync, Recipe, RecipeImport, Storage, ConnectorConfig, RecipeBook, SyncLog, Sync,
Comment, RecipeBookEntry, ShoppingList, InviteLink, UserSpace, Space Comment, RecipeBookEntry, InviteLink, UserSpace, Space
) )
for m in generic_models: for m in generic_models:
py_name = get_model_name(m) py_name = get_model_name(m)
url_name = py_name.replace('_', '-') url_name = py_name.replace('_', '-')
if c := locate(f'cookbook.views.new.{m.__name__}Create'): if c := locate(f'cookbook.views.new.{m.__name__}Create'):
urlpatterns.append( urlpatterns.append(path(f'new/{url_name}/', c.as_view(), name=f'new_{py_name}'))
path(
f'new/{url_name}/', c.as_view(), name=f'new_{py_name}'
)
)
if c := locate(f'cookbook.views.edit.{m.__name__}Update'): if c := locate(f'cookbook.views.edit.{m.__name__}Update'):
urlpatterns.append( urlpatterns.append(path(f'edit/{url_name}/<int:pk>/', c.as_view(), name=f'edit_{py_name}'))
path(
f'edit/{url_name}/<int:pk>/',
c.as_view(),
name=f'edit_{py_name}'
)
)
if c := getattr(lists, py_name, None): if c := getattr(lists, py_name, None):
urlpatterns.append( urlpatterns.append(path(f'list/{url_name}/', c, name=f'list_{py_name}'))
path(
f'list/{url_name}/', c, name=f'list_{py_name}'
)
)
if c := locate(f'cookbook.views.delete.{m.__name__}Delete'): if c := locate(f'cookbook.views.delete.{m.__name__}Delete'):
urlpatterns.append( urlpatterns.append(path(f'delete/{url_name}/<int:pk>/', c.as_view(), name=f'delete_{py_name}'))
path(
f'delete/{url_name}/<int:pk>/',
c.as_view(),
name=f'delete_{py_name}'
)
)
vue_models = [Food, Keyword, Unit, Supermarket, SupermarketCategory, Automation, UserFile, Step, CustomFilter, UnitConversion, PropertyType] vue_models = [Food, Keyword, Unit, Supermarket, SupermarketCategory, Automation, UserFile, Step, CustomFilter, UnitConversion, PropertyType]
for m in vue_models: for m in vue_models:
@@ -214,11 +176,7 @@ for m in vue_models:
url_name = py_name.replace('_', '-') url_name = py_name.replace('_', '-')
if c := getattr(lists, py_name, None): if c := getattr(lists, py_name, None):
urlpatterns.append( urlpatterns.append(path(f'list/{url_name}/', c, name=f'list_{py_name}'))
path(
f'list/{url_name}/', c, name=f'list_{py_name}'
)
)
if DEBUG: if DEBUG:
urlpatterns.append(path('test/', views.test, name='view_test')) urlpatterns.append(path('test/', views.test, name='view_test'))

View File

@@ -14,6 +14,7 @@ from zipfile import ZipFile
import requests import requests
import validators import validators
from PIL import UnidentifiedImageError
from annoying.decorators import ajax_request from annoying.decorators import ajax_request
from annoying.functions import get_object_or_None from annoying.functions import get_object_or_None
from django.contrib import messages from django.contrib import messages
@@ -30,12 +31,10 @@ from django.http import FileResponse, HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.timezone import make_aware
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
from icalendar import Calendar, Event from icalendar import Calendar, Event
from oauth2_provider.models import AccessToken from oauth2_provider.models import AccessToken
from PIL import UnidentifiedImageError
from recipe_scrapers import scrape_me from recipe_scrapers import scrape_me
from recipe_scrapers._exceptions import NoSchemaFoundInWildMode from recipe_scrapers._exceptions import NoSchemaFoundInWildMode
from requests.exceptions import MissingSchema from requests.exceptions import MissingSchema
@@ -58,23 +57,18 @@ from cookbook.helper.HelperFunctions import str2bool
from cookbook.helper.image_processing import handle_image from cookbook.helper.image_processing import handle_image
from cookbook.helper.ingredient_parser import IngredientParser from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.open_data_importer import OpenDataImporter from cookbook.helper.open_data_importer import OpenDataImporter
from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsOwner, CustomIsOwnerReadOnly, from cookbook.helper.permission_helper import (
CustomIsShared, CustomIsSpaceOwner, CustomIsUser, CustomIsAdmin, CustomIsOwner, CustomIsOwnerReadOnly, CustomIsShared, CustomIsSpaceOwner, CustomIsUser, CustomRecipePermission, CustomTokenHasReadWriteScope,
CustomRecipePermission, CustomTokenHasReadWriteScope, CustomTokenHasScope, CustomUserPermission, IsReadOnlyDRF, above_space_limit, group_required, has_group_permission, is_space_owner, switch_user_active_space,
CustomTokenHasScope, CustomUserPermission, )
IsReadOnlyDRF, above_space_limit, group_required,
has_group_permission, is_space_owner,
switch_user_active_space)
from cookbook.helper.recipe_search import RecipeSearch from cookbook.helper.recipe_search import RecipeSearch
from cookbook.helper.recipe_url_import import (clean_dict, get_from_youtube_scraper, from cookbook.helper.recipe_url_import import clean_dict, get_from_youtube_scraper, get_images_from_soup
get_images_from_soup)
from cookbook.helper.scrapers.scrapers import text_scraper from cookbook.helper.scrapers.scrapers import text_scraper
from cookbook.helper.shopping_helper import RecipeShoppingEditor, shopping_helper from cookbook.helper.shopping_helper import RecipeShoppingEditor, shopping_helper
from cookbook.models import (Automation, BookmarkletImport, CookLog, CustomFilter, ExportLog, Food, from cookbook.models import (Automation, BookmarkletImport, CookLog, CustomFilter, ExportLog, Food,
FoodInheritField, FoodProperty, ImportLog, Ingredient, InviteLink, FoodInheritField, FoodProperty, ImportLog, Ingredient, InviteLink,
Keyword, MealPlan, MealType, Property, PropertyType, Recipe, Keyword, MealPlan, MealType, Property, PropertyType, Recipe,
RecipeBook, RecipeBookEntry, ShareLink, ShoppingList, RecipeBook, RecipeBookEntry, ShareLink, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync,
SyncLog, Unit, UnitConversion, UserFile, UserPreference, UserSpace, SyncLog, Unit, UnitConversion, UserFile, UserPreference, UserSpace,
ViewLog, ConnectorConfig) ViewLog, ConnectorConfig)
@@ -96,9 +90,8 @@ from cookbook.serializer import (AccessTokenSerializer, AutomationSerializer,
RecipeExportSerializer, RecipeFromSourceSerializer, RecipeExportSerializer, RecipeFromSourceSerializer,
RecipeImageSerializer, RecipeOverviewSerializer, RecipeSerializer, RecipeImageSerializer, RecipeOverviewSerializer, RecipeSerializer,
RecipeShoppingUpdateSerializer, RecipeSimpleSerializer, RecipeShoppingUpdateSerializer, RecipeSimpleSerializer,
ShoppingListAutoSyncSerializer, ShoppingListEntrySerializer, ShoppingListEntrySerializer,
ShoppingListRecipeSerializer, ShoppingListSerializer, ShoppingListRecipeSerializer, SpaceSerializer, StepSerializer, StorageSerializer,
SpaceSerializer, StepSerializer, StorageSerializer,
SupermarketCategoryRelationSerializer, SupermarketCategoryRelationSerializer,
SupermarketCategorySerializer, SupermarketSerializer, SupermarketCategorySerializer, SupermarketSerializer,
SyncLogSerializer, SyncSerializer, UnitConversionSerializer, SyncLogSerializer, SyncSerializer, UnitConversionSerializer,
@@ -107,10 +100,11 @@ from cookbook.serializer import (AccessTokenSerializer, AutomationSerializer,
ShoppingListEntryBulkSerializer, ConnectorConfigConfigSerializer, RecipeFlatSerializer) ShoppingListEntryBulkSerializer, ConnectorConfigConfigSerializer, RecipeFlatSerializer)
from cookbook.views.import_export import get_integration from cookbook.views.import_export import get_integration
from recipes import settings from recipes import settings
from recipes.settings import FDC_API_KEY, DRF_THROTTLE_RECIPE_URL_IMPORT from recipes.settings import DRF_THROTTLE_RECIPE_URL_IMPORT, FDC_API_KEY
class StandardFilterMixin(ViewSetMixin): class StandardFilterMixin(ViewSetMixin):
def get_queryset(self): def get_queryset(self):
queryset = self.queryset queryset = self.queryset
query = self.request.query_params.get('query', None) query = self.request.query_params.get('query', None)
@@ -161,12 +155,13 @@ class ExtendedRecipeMixin():
queryset = queryset.annotate(recipe_count=Coalesce(Subquery(recipe_count), 0)) queryset = queryset.annotate(recipe_count=Coalesce(Subquery(recipe_count), 0))
# add a recipe image annotation to the query # add a recipe image annotation to the query
image_subquery = Recipe.objects.filter(**{recipe_filter: OuterRef('id')}, space=space).exclude( image_subquery = Recipe.objects.filter(**{
image__isnull=True).exclude(image__exact='').order_by("?").values('image')[:1] recipe_filter: OuterRef('id')
}, space=space).exclude(image__isnull=True).exclude(image__exact='').order_by("?").values('image')[:1]
if tree: if tree:
image_children_subquery = Recipe.objects.filter( image_children_subquery = Recipe.objects.filter(**{
**{f"{recipe_filter}__path__startswith": OuterRef('path')}, f"{recipe_filter}__path__startswith": OuterRef('path')
space=space).exclude(image__isnull=True).exclude(image__exact='').order_by("?").values('image')[:1] }, space=space).exclude(image__isnull=True).exclude(image__exact='').order_by("?").values('image')[:1]
else: else:
image_children_subquery = None image_children_subquery = None
if images: if images:
@@ -183,17 +178,17 @@ class FuzzyFilterMixin(ViewSetMixin, ExtendedRecipeMixin):
self.queryset = self.queryset.filter(space=self.request.space).order_by(Lower('name').asc()) self.queryset = self.queryset.filter(space=self.request.space).order_by(Lower('name').asc())
query = self.request.query_params.get('query', None) query = self.request.query_params.get('query', None)
if self.request.user.is_authenticated: if self.request.user.is_authenticated:
fuzzy = self.request.user.searchpreference.lookup or any([self.model.__name__.lower() in x for x in fuzzy = self.request.user.searchpreference.lookup or any(
self.request.user.searchpreference.trigram.values_list( [self.model.__name__.lower() in x for x in self.request.user.searchpreference.trigram.values_list('field', flat=True)])
'field', flat=True)])
else: else:
fuzzy = True fuzzy = True
if query is not None and query not in ["''", '']: if query is not None and query not in ["''", '']:
if fuzzy and (settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql'): if fuzzy and (settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql'):
if self.request.user.is_authenticated and any( if self.request.user.is_authenticated and any(
[self.model.__name__.lower() in x for x in self.request.user.searchpreference.unaccent.values_list('field', flat=True)]): [self.model.__name__.lower() in x for x in self.request.user.searchpreference.unaccent.values_list('field', flat=True)]
self.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name__unaccent', query)) ):
self.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name__unaccent', query))
else: else:
self.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name', query)) self.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name', query))
self.queryset = self.queryset.order_by('-trigram') self.queryset = self.queryset.order_by('-trigram')
@@ -205,10 +200,9 @@ class FuzzyFilterMixin(ViewSetMixin, ExtendedRecipeMixin):
filter |= Q(name__unaccent__icontains=query) filter |= Q(name__unaccent__icontains=query)
self.queryset = ( self.queryset = (
self.queryset.annotate(starts=Case(When(name__istartswith=query, then=(Value(100))), self.queryset.annotate(starts=Case(When(name__istartswith=query, then=(Value(100))), default=Value(0))) # put exact matches at the top of the result set
default=Value(0))) # put exact matches at the top of the result set .filter(filter).order_by('-starts',
.filter(filter).order_by('-starts', Lower('name').asc()) Lower('name').asc()))
)
updated_at = self.request.query_params.get('updated_at', None) updated_at = self.request.query_params.get('updated_at', None)
if updated_at is not None: if updated_at is not None:
@@ -229,6 +223,7 @@ class FuzzyFilterMixin(ViewSetMixin, ExtendedRecipeMixin):
class MergeMixin(ViewSetMixin): class MergeMixin(ViewSetMixin):
@decorators.action(detail=True, url_path='merge/(?P<target>[^/.]+)', methods=['PUT'], ) @decorators.action(detail=True, url_path='merge/(?P<target>[^/.]+)', methods=['PUT'], )
@decorators.renderer_classes((TemplateHTMLRenderer, JSONRenderer)) @decorators.renderer_classes((TemplateHTMLRenderer, JSONRenderer))
def merge(self, request, pk, target): def merge(self, request, pk, target):
@@ -296,8 +291,7 @@ class MergeMixin(ViewSetMixin):
return Response(content, status=status.HTTP_200_OK) return Response(content, status=status.HTTP_200_OK)
except Exception: except Exception:
traceback.print_exc() traceback.print_exc()
content = {'error': True, content = {'error': True, 'msg': _(f'An error occurred attempting to merge {source.name} with {target.name}')}
'msg': _(f'An error occurred attempting to merge {source.name} with {target.name}')}
return Response(content, status=status.HTTP_400_BAD_REQUEST) return Response(content, status=status.HTTP_400_BAD_REQUEST)
@@ -330,8 +324,7 @@ class TreeMixin(MergeMixin, FuzzyFilterMixin, ExtendedRecipeMixin):
return self.annotate_recipe(queryset=super().get_queryset(), request=self.request, serializer=self.serializer_class, tree=True) return self.annotate_recipe(queryset=super().get_queryset(), request=self.request, serializer=self.serializer_class, tree=True)
self.queryset = self.queryset.filter(space=self.request.space).order_by(Lower('name').asc()) self.queryset = self.queryset.filter(space=self.request.space).order_by(Lower('name').asc())
return self.annotate_recipe(queryset=self.queryset, request=self.request, serializer=self.serializer_class, return self.annotate_recipe(queryset=self.queryset, request=self.request, serializer=self.serializer_class, tree=True)
tree=True)
@decorators.action(detail=True, url_path='move/(?P<parent>[^/.]+)', methods=['PUT'], ) @decorators.action(detail=True, url_path='move/(?P<parent>[^/.]+)', methods=['PUT'], )
@decorators.renderer_classes((TemplateHTMLRenderer, JSONRenderer)) @decorators.renderer_classes((TemplateHTMLRenderer, JSONRenderer))
@@ -561,8 +554,7 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
def get_queryset(self): def get_queryset(self):
shared_users = [] shared_users = []
if c := caches['default'].get( if c := caches['default'].get(f'shopping_shared_users_{self.request.space.id}_{self.request.user.id}', None):
f'shopping_shared_users_{self.request.space.id}_{self.request.user.id}', None):
shared_users = c shared_users = c
else: else:
try: try:
@@ -573,8 +565,7 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
pass pass
self.queryset = super().get_queryset() self.queryset = super().get_queryset()
shopping_status = ShoppingListEntry.objects.filter(space=self.request.space, food=OuterRef('id'), shopping_status = ShoppingListEntry.objects.filter(space=self.request.space, food=OuterRef('id'), checked=False).values('id')
checked=False).values('id')
# onhand_status = self.queryset.annotate(onhand_status=Exists(onhand_users_set__in=[shared_users])) # onhand_status = self.queryset.annotate(onhand_status=Exists(onhand_users_set__in=[shared_users]))
return self.queryset \ return self.queryset \
.annotate(shopping_status=Exists(shopping_status)) \ .annotate(shopping_status=Exists(shopping_status)) \
@@ -595,8 +586,7 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
shared_users = list(self.request.user.get_shopping_share()) shared_users = list(self.request.user.get_shopping_share())
shared_users.append(request.user) shared_users.append(request.user)
if request.data.get('_delete', False) == 'true': if request.data.get('_delete', False) == 'true':
ShoppingListEntry.objects.filter(food=obj, checked=False, space=request.space, ShoppingListEntry.objects.filter(food=obj, checked=False, space=request.space, created_by__in=shared_users).delete()
created_by__in=shared_users).delete()
content = {'msg': _(f'{obj.name} was removed from the shopping list.')} content = {'msg': _(f'{obj.name} was removed from the shopping list.')}
return Response(content, status=status.HTTP_204_NO_CONTENT) return Response(content, status=status.HTTP_204_NO_CONTENT)
@@ -604,8 +594,7 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
unit = request.data.get('unit', None) unit = request.data.get('unit', None)
content = {'msg': _(f'{obj.name} was added to the shopping list.')} content = {'msg': _(f'{obj.name} was added to the shopping list.')}
ShoppingListEntry.objects.create(food=obj, amount=amount, unit=unit, space=request.space, ShoppingListEntry.objects.create(food=obj, amount=amount, unit=unit, space=request.space, created_by=request.user)
created_by=request.user)
return Response(content, status=status.HTTP_204_NO_CONTENT) return Response(content, status=status.HTTP_204_NO_CONTENT)
@decorators.action(detail=True, methods=['POST'], ) @decorators.action(detail=True, methods=['POST'], )
@@ -616,19 +605,32 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
""" """
food = self.get_object() food = self.get_object()
if not food.fdc_id: if not food.fdc_id:
return JsonResponse({'msg': 'Food has no FDC ID associated.'}, status=400, return JsonResponse({'msg': 'Food has no FDC ID associated.'}, status=400, json_dumps_params={'indent': 4})
json_dumps_params={'indent': 4})
response = requests.get(f'https://api.nal.usda.gov/fdc/v1/food/{food.fdc_id}?api_key={FDC_API_KEY}') response = requests.get(f'https://api.nal.usda.gov/fdc/v1/food/{food.fdc_id}?api_key={FDC_API_KEY}')
if response.status_code == 429: if response.status_code == 429:
return JsonResponse({'msg': 'API Key Rate Limit reached/exceeded, see https://api.data.gov/docs/rate-limits/ for more information. Configure your key in Tandoor using environment FDC_API_KEY variable.'}, status=429, return JsonResponse(
json_dumps_params={'indent': 4}) {
'msg':
'API Key Rate Limit reached/exceeded, see https://api.data.gov/docs/rate-limits/ for more information. \
Configure your key in Tandoor using environment FDC_API_KEY variable.'
},
status=429,
json_dumps_params={'indent': 4})
if response.status_code != 200: if response.status_code != 200:
return JsonResponse({'msg': f'Error while requesting FDC data using url https://api.nal.usda.gov/fdc/v1/food/{food.fdc_id}?api_key=****'}, status=response.status_code, return JsonResponse({'msg': f'Error while requesting FDC data using url https://api.nal.usda.gov/fdc/v1/food/{food.fdc_id}?api_key=****'},
status=response.status_code,
json_dumps_params={'indent': 4}) json_dumps_params={'indent': 4})
food.properties_food_amount = 100 food.properties_food_amount = 100
food.properties_food_unit = Unit.objects.get_or_create(base_unit__iexact='g', space=self.request.space, defaults={'name': 'g', 'base_unit': 'g', 'space': self.request.space})[0] food.properties_food_unit = Unit.objects.get_or_create(base_unit__iexact='g',
space=self.request.space,
defaults={
'name': 'g',
'base_unit': 'g',
'space': self.request.space
})[0]
food.save() food.save()
try: try:
@@ -694,8 +696,7 @@ class RecipeBookViewSet(viewsets.ModelViewSet, StandardFilterMixin):
ordering = f"{'' if order_direction == 'asc' else '-'}{order_field}" ordering = f"{'' if order_direction == 'asc' else '-'}{order_field}"
self.queryset = self.queryset.filter(Q(created_by=self.request.user) | Q(shared=self.request.user)).filter( self.queryset = self.queryset.filter(Q(created_by=self.request.user) | Q(shared=self.request.user)).filter(space=self.request.space).distinct().order_by(ordering)
space=self.request.space).distinct().order_by(ordering)
return super().get_queryset() return super().get_queryset()
@@ -713,9 +714,7 @@ class RecipeBookEntryViewSet(viewsets.ModelViewSet, viewsets.GenericViewSet):
permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope] permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope]
def get_queryset(self): def get_queryset(self):
queryset = self.queryset.filter( queryset = self.queryset.filter(Q(book__created_by=self.request.user) | Q(book__shared=self.request.user)).filter(book__space=self.request.space).distinct()
Q(book__created_by=self.request.user) | Q(book__shared=self.request.user)).filter(
book__space=self.request.space).distinct()
recipe_id = self.request.query_params.get('recipe', None) recipe_id = self.request.query_params.get('recipe', None)
if recipe_id is not None: if recipe_id is not None:
@@ -748,10 +747,7 @@ class MealPlanViewSet(viewsets.ModelViewSet):
schema = QueryParamAutoSchema() schema = QueryParamAutoSchema()
def get_queryset(self): def get_queryset(self):
queryset = self.queryset.filter( queryset = self.queryset.filter(Q(created_by=self.request.user) | Q(shared=self.request.user)).filter(space=self.request.space).distinct().all()
Q(created_by=self.request.user)
| Q(shared=self.request.user)
).filter(space=self.request.space).distinct().all()
from_date = self.request.query_params.get('from_date', None) from_date = self.request.query_params.get('from_date', None)
if from_date is not None: if from_date is not None:
@@ -769,6 +765,7 @@ class MealPlanViewSet(viewsets.ModelViewSet):
class AutoPlanViewSet(viewsets.ViewSet): class AutoPlanViewSet(viewsets.ViewSet):
def create(self, request): def create(self, request):
serializer = AutoMealPlanSerializer(data=request.data) serializer = AutoMealPlanSerializer(data=request.data)
@@ -798,10 +795,16 @@ class AutoPlanViewSet(viewsets.ViewSet):
for i in range(0, days): for i in range(0, days):
day = start_date + datetime.timedelta(i) day = start_date + datetime.timedelta(i)
recipe = recipes[i % len(recipes)] recipe = recipes[i % len(recipes)]
args = {'recipe_id': recipe['id'], 'servings': servings, args = {
'created_by': request.user, 'recipe_id': recipe['id'],
'meal_type_id': serializer.validated_data['meal_type_id'], 'servings': servings,
'note': '', 'from_date': day, 'to_date': day, 'space': request.space} 'created_by': request.user,
'meal_type_id': serializer.validated_data['meal_type_id'],
'note': '',
'from_date': day,
'to_date': day,
'space': request.space
}
m = MealPlan(**args) m = MealPlan(**args)
meal_plans.append(m) meal_plans.append(m)
@@ -816,12 +819,7 @@ class AutoPlanViewSet(viewsets.ViewSet):
SLR.create(mealplan=m, servings=servings) SLR.create(mealplan=m, servings=servings)
else: else:
post_save.send( post_save.send(sender=m.__class__, instance=m, created=True, update_fields=None, )
sender=m.__class__,
instance=m,
created=True,
update_fields=None,
)
return Response(serializer.data) return Response(serializer.data)
@@ -838,8 +836,7 @@ class MealTypeViewSet(viewsets.ModelViewSet):
permission_classes = [CustomIsOwner & CustomTokenHasReadWriteScope] permission_classes = [CustomIsOwner & CustomTokenHasReadWriteScope]
def get_queryset(self): def get_queryset(self):
queryset = self.queryset.order_by('order', 'id').filter(created_by=self.request.user).filter( queryset = self.queryset.order_by('order', 'id').filter(created_by=self.request.user).filter(space=self.request.space).all()
space=self.request.space).all()
return queryset return queryset
@@ -899,12 +896,7 @@ class RecipePagination(PageNumberPagination):
return super().paginate_queryset(queryset, request, view) return super().paginate_queryset(queryset, request, view)
def get_paginated_response(self, data): def get_paginated_response(self, data):
return Response(OrderedDict([ return Response(OrderedDict([('count', self.page.paginator.count), ('next', self.get_next_link()), ('previous', self.get_previous_link()), ('results', data), ]))
('count', self.page.paginator.count),
('next', self.get_next_link()),
('previous', self.get_previous_link()),
('results', data),
]))
class RecipeViewSet(viewsets.ModelViewSet): class RecipeViewSet(viewsets.ModelViewSet):
@@ -950,49 +942,32 @@ class RecipeViewSet(viewsets.ModelViewSet):
if self.detail: # if detail request and not list, private condition is verified by permission class if self.detail: # if detail request and not list, private condition is verified by permission class
if not share: # filter for space only if not shared if not share: # filter for space only if not shared
self.queryset = self.queryset.filter(space=self.request.space).prefetch_related( self.queryset = self.queryset.filter(
'keywords', space=self.request.space).prefetch_related('keywords', 'shared', 'properties', 'properties__property_type', 'steps', 'steps__ingredients',
'shared', 'steps__ingredients__step_set', 'steps__ingredients__step_set__recipe_set', 'steps__ingredients__food',
'properties', 'steps__ingredients__food__properties', 'steps__ingredients__food__properties__property_type',
'properties__property_type', 'steps__ingredients__food__inherit_fields', 'steps__ingredients__food__supermarket_category',
'steps', 'steps__ingredients__food__onhand_users', 'steps__ingredients__food__substitute',
'steps__ingredients', 'steps__ingredients__food__child_inherit_fields', 'steps__ingredients__unit',
'steps__ingredients__step_set', 'steps__ingredients__unit__unit_conversion_base_relation',
'steps__ingredients__step_set__recipe_set', 'steps__ingredients__unit__unit_conversion_base_relation__base_unit',
'steps__ingredients__food', 'steps__ingredients__unit__unit_conversion_converted_relation',
'steps__ingredients__food__properties', 'steps__ingredients__unit__unit_conversion_converted_relation__converted_unit', 'cooklog_set',
'steps__ingredients__food__properties__property_type', ).select_related('nutrition')
'steps__ingredients__food__inherit_fields',
'steps__ingredients__food__supermarket_category',
'steps__ingredients__food__onhand_users',
'steps__ingredients__food__substitute',
'steps__ingredients__food__child_inherit_fields',
'steps__ingredients__unit',
'steps__ingredients__unit__unit_conversion_base_relation',
'steps__ingredients__unit__unit_conversion_base_relation__base_unit',
'steps__ingredients__unit__unit_conversion_converted_relation',
'steps__ingredients__unit__unit_conversion_converted_relation__converted_unit',
'cooklog_set',
).select_related('nutrition')
return super().get_queryset() return super().get_queryset()
self.queryset = self.queryset.filter(space=self.request.space).filter( self.queryset = self.queryset.filter(
Q(private=False) | (Q(private=True) & (Q(created_by=self.request.user) | Q(shared=self.request.user))) space=self.request.space).filter(Q(private=False) | (Q(private=True) & (Q(created_by=self.request.user) | Q(shared=self.request.user))))
)
params = {x: self.request.GET.get(x) if len({**self.request.GET}[x]) == 1 else self.request.GET.getlist(x) for x 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)}
in list(self.request.GET)}
search = RecipeSearch(self.request, **params) search = RecipeSearch(self.request, **params)
self.queryset = search.get_queryset(self.queryset).prefetch_related('keywords', 'cooklog_set') self.queryset = search.get_queryset(self.queryset).prefetch_related('keywords', 'cooklog_set')
return self.queryset return self.queryset
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
if self.request.GET.get('debug', False): if self.request.GET.get('debug', False):
return JsonResponse({ return JsonResponse({'new': str(self.get_queryset().query), })
'new': str(self.get_queryset().query),
})
return super().list(request, *args, **kwargs) return super().list(request, *args, **kwargs)
def get_serializer_class(self): def get_serializer_class(self):
@@ -1000,12 +975,7 @@ class RecipeViewSet(viewsets.ModelViewSet):
return RecipeOverviewSerializer return RecipeOverviewSerializer
return self.serializer_class return self.serializer_class
@decorators.action( @decorators.action(detail=True, methods=['PUT'], serializer_class=RecipeImageSerializer, parser_classes=[MultiPartParser], )
detail=True,
methods=['PUT'],
serializer_class=RecipeImageSerializer,
parser_classes=[MultiPartParser],
)
def image(self, request, pk): def image(self, request, pk):
obj = self.get_object() obj = self.get_object()
@@ -1053,11 +1023,7 @@ class RecipeViewSet(viewsets.ModelViewSet):
# TODO: refactor API to use post/put/delete or leave as put and change VUE to use list_recipe after creating # TODO: refactor API to use post/put/delete or leave as put and change VUE to use list_recipe after creating
# DRF only allows one action in a decorator action without overriding get_operation_id_base() # DRF only allows one action in a decorator action without overriding get_operation_id_base()
@decorators.action( @decorators.action(detail=True, methods=['PUT'], serializer_class=RecipeShoppingUpdateSerializer, )
detail=True,
methods=['PUT'],
serializer_class=RecipeShoppingUpdateSerializer,
)
def shopping(self, request, pk): def shopping(self, request, pk):
if self.request.space.demo: if self.request.space.demo:
raise PermissionDenied(detail='Not available in demo', code=None) raise PermissionDenied(detail='Not available in demo', code=None)
@@ -1087,11 +1053,7 @@ class RecipeViewSet(viewsets.ModelViewSet):
return Response(content, status=http_status) return Response(content, status=http_status)
@decorators.action( @decorators.action(detail=True, methods=['GET'], serializer_class=RecipeSimpleSerializer)
detail=True,
methods=['GET'],
serializer_class=RecipeSimpleSerializer
)
def related(self, request, pk): def related(self, request, pk):
obj = self.get_object() obj = self.get_object()
if obj.get_space() != request.space: if obj.get_space() != request.space:
@@ -1100,8 +1062,7 @@ class RecipeViewSet(viewsets.ModelViewSet):
levels = int(request.query_params.get('levels', 1)) levels = int(request.query_params.get('levels', 1))
except (ValueError, TypeError): except (ValueError, TypeError):
levels = 1 levels = 1
qs = obj.get_related_recipes( qs = obj.get_related_recipes(levels=levels) # TODO: make levels a user setting, included in request data?, keep solely in the backend?
levels=levels) # TODO: make levels a user setting, included in request data?, keep solely in the backend?
return Response(self.serializer_class(qs, many=True).data) return Response(self.serializer_class(qs, many=True).data)
@decorators.action( @decorators.action(
@@ -1157,14 +1118,12 @@ class ShoppingListRecipeViewSet(viewsets.ModelViewSet):
permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope] permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope]
def get_queryset(self): def get_queryset(self):
self.queryset = self.queryset.filter( self.queryset = self.queryset.filter(Q(entries__space=self.request.space) | Q(recipe__space=self.request.space))
Q(shoppinglist__space=self.request.space) | Q(entries__space=self.request.space))
return self.queryset.filter( return self.queryset.filter(
Q(shoppinglist__created_by=self.request.user) Q(entries__isnull=True)
| Q(shoppinglist__shared=self.request.user)
| Q(entries__created_by=self.request.user) | Q(entries__created_by=self.request.user)
| Q(entries__created_by__in=list(self.request.user.get_shopping_share())) | Q(entries__created_by__in=list(self.request.user.get_shopping_share()))
).distinct().all() ).distinct().all()
class ShoppingListEntryViewSet(viewsets.ModelViewSet): class ShoppingListEntryViewSet(viewsets.ModelViewSet):
@@ -1173,7 +1132,10 @@ class ShoppingListEntryViewSet(viewsets.ModelViewSet):
permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope] permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope]
query_params = [ query_params = [
QueryParam(name='id', description=_('Returns the shopping list entry with a primary key of id. Multiple values allowed.'), qtype='integer'), QueryParam(name='id', description=_('Returns the shopping list entry with a primary key of id. Multiple values allowed.'), qtype='integer'),
QueryParam(name='checked', description=_('Filter shopping list entries on checked. [''true'', ''false'', ''both'', ''<b>recent</b>'']<br> - ''recent'' includes unchecked items and recently completed items.') QueryParam(
name='checked',
description=_('Filter shopping list entries on checked. [''true'', ''false'', ''both'', ''<b>recent</b>'']<br> \
- ''recent'' includes unchecked items and recently completed items.')
), ),
QueryParam(name='supermarket', description=_('Returns the shopping list entries sorted by supermarket category order.'), qtype='integer'), QueryParam(name='supermarket', description=_('Returns the shopping list entries sorted by supermarket category order.'), qtype='integer'),
] ]
@@ -1184,24 +1146,11 @@ class ShoppingListEntryViewSet(viewsets.ModelViewSet):
self.queryset = self.queryset.filter( self.queryset = self.queryset.filter(
Q(created_by=self.request.user) Q(created_by=self.request.user)
| Q(shoppinglist__shared=self.request.user) | Q(created_by__in=list(self.request.user.get_shopping_share()))).prefetch_related('created_by', 'food', 'food__properties', 'food__properties__property_type',
| Q(created_by__in=list(self.request.user.get_shopping_share())) 'food__inherit_fields', 'food__supermarket_category', 'food__onhand_users',
).prefetch_related( 'food__substitute', 'food__child_inherit_fields', 'unit', 'list_recipe',
'created_by', 'list_recipe__mealplan', 'list_recipe__mealplan__recipe',
'food', ).distinct().all()
'food__properties',
'food__properties__property_type',
'food__inherit_fields',
'food__supermarket_category',
'food__onhand_users',
'food__substitute',
'food__child_inherit_fields',
'unit',
'list_recipe',
'list_recipe__mealplan',
'list_recipe__mealplan__recipe',
).distinct().all()
if pk := self.request.query_params.getlist('id', []): if pk := self.request.query_params.getlist('id', []):
self.queryset = self.queryset.filter(food__id__in=[int(i) for i in pk]) self.queryset = self.queryset.filter(food__id__in=[int(i) for i in pk])
@@ -1218,7 +1167,7 @@ class ShoppingListEntryViewSet(viewsets.ModelViewSet):
if last_autosync: if last_autosync:
last_autosync = datetime.datetime.fromtimestamp(int(last_autosync) / 1000, datetime.timezone.utc) last_autosync = datetime.datetime.fromtimestamp(int(last_autosync) / 1000, datetime.timezone.utc)
self.queryset = self.queryset.filter(updated_at__gte=last_autosync) self.queryset = self.queryset.filter(updated_at__gte=last_autosync)
except: except Exception:
traceback.print_exc() traceback.print_exc()
# TODO once old shopping list is removed this needs updated to sharing users in preferences # TODO once old shopping list is removed this needs updated to sharing users in preferences
@@ -1227,53 +1176,20 @@ class ShoppingListEntryViewSet(viewsets.ModelViewSet):
else: else:
return self.queryset[:1000] return self.queryset[:1000]
@decorators.action( @decorators.action(detail=False, methods=['POST'], serializer_class=ShoppingListEntryBulkSerializer, permission_classes=[CustomIsUser])
detail=False,
methods=['POST'],
serializer_class=ShoppingListEntryBulkSerializer,
permission_classes=[CustomIsUser]
)
def bulk(self, request): def bulk(self, request):
serializer = self.serializer_class(data=request.data) serializer = self.serializer_class(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
ShoppingListEntry.objects.filter( ShoppingListEntry.objects.filter(Q(created_by=self.request.user)
Q(created_by=self.request.user) | Q(created_by__in=list(self.request.user.get_shopping_share()))).filter(space=request.space, id__in=serializer.validated_data['ids']
| Q(shoppinglist__shared=self.request.user) ).update(checked=serializer.validated_data['checked'],
| Q(created_by__in=list(self.request.user.get_shopping_share())) updated_at=timezone.now(),
).filter(space=request.space, id__in=serializer.validated_data['ids']).update( )
checked=serializer.validated_data['checked'],
updated_at=timezone.now(),
)
return Response(serializer.data) return Response(serializer.data)
else: else:
return Response(serializer.errors, 400) return Response(serializer.errors, 400)
# TODO deprecate
class ShoppingListViewSet(viewsets.ModelViewSet):
queryset = ShoppingList.objects
serializer_class = ShoppingListSerializer
permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope]
def get_queryset(self):
self.queryset = self.queryset.filter(
Q(created_by=self.request.user)
| Q(shared=self.request.user)
| Q(created_by__in=list(self.request.user.get_shopping_share()))
).filter(space=self.request.space)
return self.queryset.distinct()
def get_serializer_class(self):
try:
autosync = self.request.query_params.get('autosync', False)
if autosync:
return ShoppingListAutoSyncSerializer
except AttributeError: # Needed for the openapi schema to determine a serializer without a request
pass
return self.serializer_class
class ViewLogViewSet(viewsets.ModelViewSet): class ViewLogViewSet(viewsets.ModelViewSet):
queryset = ViewLog.objects queryset = ViewLog.objects
serializer_class = ViewLogSerializer serializer_class = ViewLogSerializer
@@ -1348,11 +1264,52 @@ class UserFileViewSet(viewsets.ModelViewSet, StandardFilterMixin):
class AutomationViewSet(viewsets.ModelViewSet, StandardFilterMixin): class AutomationViewSet(viewsets.ModelViewSet, StandardFilterMixin):
"""
list:
optional parameters
- **automation_type**: Return the Automations matching the automation type. Multiple values allowed.
*Automation Types:*
- FS: Food Alias
- UA: Unit Alias
- KA: Keyword Alias
- DR: Description Replace
- IR: Instruction Replace
- NU: Never Unit
- TW: Transpose Words
- FR: Food Replace
- UR: Unit Replace
- NR: Name Replace
"""
queryset = Automation.objects queryset = Automation.objects
serializer_class = AutomationSerializer serializer_class = AutomationSerializer
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope] permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
pagination_class = DefaultPagination
query_params = [
QueryParam(name='automation_type', description=_('Return the Automations matching the automation type. Multiple values allowed.'), qtype='string'),
]
schema = QueryParamAutoSchema()
auto_type = {
'FS': 'FOOD_ALIAS',
'UA': 'UNIT_ALIAS',
'KA': 'KEYWORD_ALIAS',
'DR': 'DESCRIPTION_REPLACE',
'IR': 'INSTRUCTION_REPLACE',
'NU': 'NEVER_UNIT',
'TW': 'TRANSPOSE_WORDS',
'FR': 'FOOD_REPLACE',
'UR': 'UNIT_REPLACE',
'NR': 'NAME_REPLACE'
}
def get_queryset(self): def get_queryset(self):
automation_type = self.request.query_params.getlist('automation_type', [])
if automation_type:
self.queryset = self.queryset.filter(type__in=[self.auto_type[x.upper()] for x in automation_type])
self.queryset = self.queryset.filter(space=self.request.space).all() self.queryset = self.queryset.filter(space=self.request.space).all()
return super().get_queryset() return super().get_queryset()
@@ -1379,10 +1336,10 @@ class CustomFilterViewSet(viewsets.ModelViewSet, StandardFilterMixin):
queryset = CustomFilter.objects queryset = CustomFilter.objects
serializer_class = CustomFilterSerializer serializer_class = CustomFilterSerializer
permission_classes = [CustomIsOwner & CustomTokenHasReadWriteScope] permission_classes = [CustomIsOwner & CustomTokenHasReadWriteScope]
pagination_class = DefaultPagination
def get_queryset(self): def get_queryset(self):
self.queryset = self.queryset.filter(Q(created_by=self.request.user) | Q(shared=self.request.user)).filter( self.queryset = self.queryset.filter(Q(created_by=self.request.user) | Q(shared=self.request.user)).filter(space=self.request.space).distinct()
space=self.request.space).distinct()
return super().get_queryset() return super().get_queryset()
@@ -1397,6 +1354,7 @@ class AccessTokenViewSet(viewsets.ModelViewSet):
# -------------- DRF custom views -------------------- # -------------- DRF custom views --------------------
class AuthTokenThrottle(AnonRateThrottle): class AuthTokenThrottle(AnonRateThrottle):
rate = '10/day' rate = '10/day'
@@ -1409,15 +1367,14 @@ class CustomAuthToken(ObtainAuthToken):
throttle_classes = [AuthTokenThrottle] throttle_classes = [AuthTokenThrottle]
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data, serializer = self.serializer_class(data=request.data, context={'request': request})
context={'request': request})
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
user = serializer.validated_data['user'] user = serializer.validated_data['user']
if token := AccessToken.objects.filter(user=user, expires__gt=timezone.now(), scope__contains='read').filter( if token := AccessToken.objects.filter(user=user, expires__gt=timezone.now(), scope__contains='read').filter(scope__contains='write').first():
scope__contains='write').first():
access_token = token access_token = token
else: else:
access_token = AccessToken.objects.create(user=user, token=f'tda_{str(uuid.uuid4()).replace("-", "_")}', access_token = AccessToken.objects.create(user=user,
token=f'tda_{str(uuid.uuid4()).replace("-", "_")}',
expires=(timezone.now() + timezone.timedelta(days=365 * 5)), expires=(timezone.now() + timezone.timedelta(days=365 * 5)),
scope='read write app') scope='read write app')
return Response({ return Response({
@@ -1447,8 +1404,7 @@ class RecipeUrlImportView(APIView):
serializer = RecipeFromSourceSerializer(data=request.data) serializer = RecipeFromSourceSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
if (b_pk := serializer.validated_data.get('bookmarklet', None)) and ( if (b_pk := serializer.validated_data.get('bookmarklet', None)) and (bookmarklet := BookmarkletImport.objects.filter(pk=b_pk).first()):
bookmarklet := BookmarkletImport.objects.filter(pk=b_pk).first()):
serializer.validated_data['url'] = bookmarklet.url serializer.validated_data['url'] = bookmarklet.url
serializer.validated_data['data'] = bookmarklet.html serializer.validated_data['data'] = bookmarklet.html
bookmarklet.delete() bookmarklet.delete()
@@ -1456,61 +1412,40 @@ class RecipeUrlImportView(APIView):
url = serializer.validated_data.get('url', None) url = serializer.validated_data.get('url', None)
data = unquote(serializer.validated_data.get('data', None)) data = unquote(serializer.validated_data.get('data', None))
if not url and not data: if not url and not data:
return Response({ return Response({'error': True, 'msg': _('Nothing to do.')}, status=status.HTTP_400_BAD_REQUEST)
'error': True,
'msg': _('Nothing to do.')
}, status=status.HTTP_400_BAD_REQUEST)
elif url and not data: elif url and not data:
if re.match('^(https?://)?(www\\.youtube\\.com|youtu\\.be)/.+$', url): if re.match('^(https?://)?(www\\.youtube\\.com|youtu\\.be)/.+$', url):
if validators.url(url, public=True): if validators.url(url, public=True):
return Response({ return Response({'recipe_json': get_from_youtube_scraper(url, request), 'recipe_images': [], }, status=status.HTTP_200_OK)
'recipe_json': get_from_youtube_scraper(url, request), if re.match('^(.)*/view/recipe/[0-9]+/[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$', url):
'recipe_images': [],
}, status=status.HTTP_200_OK)
if re.match(
'^(.)*/view/recipe/[0-9]+/[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$',
url):
recipe_json = requests.get( recipe_json = requests.get(
url.replace('/view/recipe/', '/api/recipe/').replace(re.split('/view/recipe/[0-9]+', url)[1], url.replace('/view/recipe/', '/api/recipe/').replace(re.split('/view/recipe/[0-9]+', url)[1], '') + '?share='
'') + '?share=' + + re.split('/view/recipe/[0-9]+', url)[1].replace('/', '')).json()
re.split('/view/recipe/[0-9]+', url)[1].replace('/', '')).json()
recipe_json = clean_dict(recipe_json, 'id') recipe_json = clean_dict(recipe_json, 'id')
serialized_recipe = RecipeExportSerializer(data=recipe_json, context={'request': request}) serialized_recipe = RecipeExportSerializer(data=recipe_json, context={'request': request})
if serialized_recipe.is_valid(): if serialized_recipe.is_valid():
recipe = serialized_recipe.save() recipe = serialized_recipe.save()
if validators.url(recipe_json['image'], public=True): if validators.url(recipe_json['image'], public=True):
recipe.image = File(handle_image(request, recipe.image = File(handle_image(request,
File(io.BytesIO(requests.get(recipe_json['image']).content), File(io.BytesIO(requests.get(recipe_json['image']).content), name='image'),
name='image'),
filetype=pathlib.Path(recipe_json['image']).suffix), filetype=pathlib.Path(recipe_json['image']).suffix),
name=f'{uuid.uuid4()}_{recipe.pk}{pathlib.Path(recipe_json["image"]).suffix}') name=f'{uuid.uuid4()}_{recipe.pk}{pathlib.Path(recipe_json["image"]).suffix}')
recipe.save() recipe.save()
return Response({ return Response({'link': request.build_absolute_uri(reverse('view_recipe', args={recipe.pk}))}, status=status.HTTP_201_CREATED)
'link': request.build_absolute_uri(reverse('view_recipe', args={recipe.pk}))
}, status=status.HTTP_201_CREATED)
else: else:
try: try:
if validators.url(url, public=True): if validators.url(url, public=True):
scrape = scrape_me(url_path=url, wild_mode=True) scrape = scrape_me(url_path=url, wild_mode=True)
else: else:
return Response({ return Response({'error': True, 'msg': _('Invalid Url')}, status=status.HTTP_400_BAD_REQUEST)
'error': True,
'msg': _('Invalid Url')
}, status=status.HTTP_400_BAD_REQUEST)
except NoSchemaFoundInWildMode: except NoSchemaFoundInWildMode:
pass pass
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
return Response({ return Response({'error': True, 'msg': _('Connection Refused.')}, status=status.HTTP_400_BAD_REQUEST)
'error': True,
'msg': _('Connection Refused.')
}, status=status.HTTP_400_BAD_REQUEST)
except requests.exceptions.MissingSchema: except requests.exceptions.MissingSchema:
return Response({ return Response({'error': True, 'msg': _('Bad URL Schema.')}, status=status.HTTP_400_BAD_REQUEST)
'error': True,
'msg': _('Bad URL Schema.')
}, status=status.HTTP_400_BAD_REQUEST)
else: else:
try: try:
data_json = json.loads(data) data_json = json.loads(data)
@@ -1529,13 +1464,11 @@ class RecipeUrlImportView(APIView):
return Response({ return Response({
'recipe_json': helper.get_from_scraper(scrape, request), 'recipe_json': helper.get_from_scraper(scrape, request),
'recipe_images': list(dict.fromkeys(get_images_from_soup(scrape.soup, url))), 'recipe_images': list(dict.fromkeys(get_images_from_soup(scrape.soup, url))),
}, status=status.HTTP_200_OK) },
status=status.HTTP_200_OK)
else: else:
return Response({ return Response({'error': True, 'msg': _('No usable data could be found.')}, status=status.HTTP_400_BAD_REQUEST)
'error': True,
'msg': _('No usable data could be found.')
}, status=status.HTTP_400_BAD_REQUEST)
else: else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -1627,8 +1560,7 @@ def import_files(request):
return Response({'import_id': il.pk}, status=status.HTTP_200_OK) return Response({'import_id': il.pk}, status=status.HTTP_200_OK)
except NotImplementedError: except NotImplementedError:
return Response({'error': True, 'msg': _('Importing is not implemented for this provider')}, return Response({'error': True, 'msg': _('Importing is not implemented for this provider')}, status=status.HTTP_400_BAD_REQUEST)
status=status.HTTP_400_BAD_REQUEST)
else: else:
return Response({'error': True, 'msg': form.errors}, status=status.HTTP_400_BAD_REQUEST) return Response({'error': True, 'msg': form.errors}, status=status.HTTP_400_BAD_REQUEST)
@@ -1712,8 +1644,7 @@ def get_recipe_file(request, recipe_id):
@group_required('user') @group_required('user')
def sync_all(request): def sync_all(request):
if request.space.demo or settings.HOSTED: if request.space.demo or settings.HOSTED:
messages.add_message(request, messages.ERROR, messages.add_message(request, messages.ERROR, _('This feature is not yet available in the hosted version of tandoor!'))
_('This feature is not yet available in the hosted version of tandoor!'))
return redirect('index') return redirect('index')
monitors = Sync.objects.filter(active=True).filter(space=request.user.userspace_set.filter(active=1).first().space) monitors = Sync.objects.filter(active=True).filter(space=request.user.userspace_set.filter(active=1).first().space)
@@ -1734,14 +1665,10 @@ def sync_all(request):
error = True error = True
if not error: if not error:
messages.add_message( messages.add_message(request, messages.SUCCESS, _('Sync successful!'))
request, messages.SUCCESS, _('Sync successful!')
)
return redirect('list_recipe_import') return redirect('list_recipe_import')
else: else:
messages.add_message( messages.add_message(request, messages.ERROR, _('Error synchronizing with Storage'))
request, messages.ERROR, _('Error synchronizing with Storage')
)
return redirect('list_recipe_import') return redirect('list_recipe_import')
@@ -1749,11 +1676,10 @@ def sync_all(request):
# @schema(AutoSchema()) #TODO add proper schema # @schema(AutoSchema()) #TODO add proper schema
@permission_classes([CustomIsUser & CustomTokenHasReadWriteScope]) @permission_classes([CustomIsUser & CustomTokenHasReadWriteScope])
def share_link(request, pk): def share_link(request, pk):
if request.space.allow_sharing and has_group_permission(request.user, ('user',)): if request.space.allow_sharing and has_group_permission(request.user, ('user', )):
recipe = get_object_or_404(Recipe, pk=pk, space=request.space) recipe = get_object_or_404(Recipe, pk=pk, space=request.space)
link = ShareLink.objects.create(recipe=recipe, created_by=request.user, space=request.space) link = ShareLink.objects.create(recipe=recipe, created_by=request.user, space=request.space)
return JsonResponse({'pk': pk, 'share': link.uuid, return JsonResponse({'pk': pk, 'share': link.uuid, 'link': request.build_absolute_uri(reverse('view_recipe', args=[pk, link.uuid]))})
'link': request.build_absolute_uri(reverse('view_recipe', args=[pk, link.uuid]))})
else: else:
return JsonResponse({'error': 'sharing_disabled'}, status=403) return JsonResponse({'error': 'sharing_disabled'}, status=403)
@@ -1779,9 +1705,8 @@ def log_cooking(request, recipe_id):
@group_required('user') @group_required('user')
def get_plan_ical(request, from_date, to_date): def get_plan_ical(request, from_date, to_date):
queryset = MealPlan.objects.filter( queryset = MealPlan.objects.filter(Q(created_by=request.user)
Q(created_by=request.user) | Q(shared=request.user) | Q(shared=request.user)).filter(space=request.user.userspace_set.filter(active=1).first().space).distinct().all()
).filter(space=request.user.userspace_set.filter(active=1).first().space).distinct().all()
if from_date is not None: if from_date is not None:
queryset = queryset.filter(from_date__gte=from_date) queryset = queryset.filter(from_date__gte=from_date)
@@ -1822,12 +1747,4 @@ def ingredient_from_string(request):
ingredient_parser = IngredientParser(request, False) ingredient_parser = IngredientParser(request, False)
amount, unit, food, note = ingredient_parser.parse(text) amount, unit, food, note = ingredient_parser.parse(text)
return JsonResponse( return JsonResponse({'amount': amount, 'unit': unit, 'food': food, 'note': note}, status=200)
{
'amount': amount,
'unit': unit,
'food': food,
'note': note
},
status=200
)

View File

@@ -8,8 +8,12 @@ from django.utils.translation import gettext as _
from django.views.generic import DeleteView from django.views.generic import DeleteView
from cookbook.helper.permission_helper import GroupRequiredMixin, OwnerRequiredMixin, group_required from cookbook.helper.permission_helper import GroupRequiredMixin, OwnerRequiredMixin, group_required
from cookbook.models import Comment, InviteLink, Recipe, RecipeImport, Space, Storage, Sync, UserSpace
from cookbook.models import (Comment, InviteLink, MealPlan, Recipe, RecipeBook, RecipeBookEntry, from cookbook.models import (Comment, InviteLink, MealPlan, Recipe, RecipeBook, RecipeBookEntry,
RecipeImport, Space, Storage, Sync, UserSpace, ConnectorConfig) RecipeImport, Space, Storage, Sync, UserSpace, ConnectorConfig)
from cookbook.provider.dropbox import Dropbox from cookbook.provider.dropbox import Dropbox
from cookbook.provider.local import Local from cookbook.provider.local import Local
from cookbook.provider.nextcloud import Nextcloud from cookbook.provider.nextcloud import Nextcloud
@@ -114,11 +118,9 @@ class StorageDelete(GroupRequiredMixin, DeleteView):
try: try:
return self.delete(request, *args, **kwargs) return self.delete(request, *args, **kwargs)
except ProtectedError: except ProtectedError:
messages.add_message( messages.add_message(request, messages.WARNING,
request, _('Could not delete this storage backend as it is used in at least one monitor.') # noqa: E501
messages.WARNING, )
_('Could not delete this storage backend as it is used in at least one monitor.') # noqa: E501
)
return HttpResponseRedirect(reverse('list_storage')) return HttpResponseRedirect(reverse('list_storage'))
@@ -145,40 +147,6 @@ class CommentDelete(OwnerRequiredMixin, DeleteView):
return context return context
class RecipeBookDelete(OwnerRequiredMixin, DeleteView):
template_name = "generic/delete_template.html"
model = RecipeBook
success_url = reverse_lazy('view_books')
def get_context_data(self, **kwargs):
context = super(RecipeBookDelete, self).get_context_data(**kwargs)
context['title'] = _("Recipe Book")
return context
class RecipeBookEntryDelete(OwnerRequiredMixin, DeleteView):
groups_required = ['user']
template_name = "generic/delete_template.html"
model = RecipeBookEntry
success_url = reverse_lazy('view_books')
def get_context_data(self, **kwargs):
context = super(RecipeBookEntryDelete, self).get_context_data(**kwargs)
context['title'] = _("Bookmarks")
return context
class MealPlanDelete(OwnerRequiredMixin, DeleteView):
template_name = "generic/delete_template.html"
model = MealPlan
success_url = reverse_lazy('view_plan')
def get_context_data(self, **kwargs):
context = super(MealPlanDelete, self).get_context_data(**kwargs)
context['title'] = _("Meal-Plan")
return context
class InviteLinkDelete(OwnerRequiredMixin, DeleteView): class InviteLinkDelete(OwnerRequiredMixin, DeleteView):
template_name = "generic/delete_template.html" template_name = "generic/delete_template.html"
model = InviteLink model = InviteLink

View File

@@ -15,7 +15,7 @@ class RecipeCreate(GroupRequiredMixin, CreateView):
groups_required = ['user'] groups_required = ['user']
template_name = "generic/new_template.html" template_name = "generic/new_template.html"
model = Recipe model = Recipe
fields = ('name',) fields = ('name', )
def form_valid(self, form): def form_valid(self, form):
limit, msg = above_space_limit(self.request.space) limit, msg = above_space_limit(self.request.space)
@@ -126,12 +126,6 @@ def create_new_external_recipe(request, import_id):
messages.add_message(request, messages.ERROR, _('There was an error importing this recipe!')) messages.add_message(request, messages.ERROR, _('There was an error importing this recipe!'))
else: else:
new_recipe = get_object_or_404(RecipeImport, pk=import_id, space=request.space) new_recipe = get_object_or_404(RecipeImport, pk=import_id, space=request.space)
form = ImportRecipeForm( form = ImportRecipeForm(initial={'file_path': new_recipe.file_path, 'name': new_recipe.name, 'file_uid': new_recipe.file_uid}, space=request.space)
initial={
'file_path': new_recipe.file_path,
'name': new_recipe.name,
'file_uid': new_recipe.file_uid
}, space=request.space
)
return render(request, 'forms/edit_import_recipe.html', {'form': form}) return render(request, 'forms/edit_import_recipe.html', {'form': form})

View File

@@ -1,7 +1,5 @@
import json
import os import os
import re import re
import subprocess
from datetime import datetime from datetime import datetime
from io import StringIO from io import StringIO
from uuid import UUID from uuid import UUID
@@ -17,7 +15,6 @@ from django.core.management import call_command
from django.db import models from django.db import models
from django.http import HttpResponseRedirect, JsonResponse from django.http import HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.templatetags.static import static
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
@@ -30,7 +27,7 @@ from cookbook.models import Comment, CookLog, InviteLink, SearchFields, SearchPr
from cookbook.tables import CookLogTable, ViewLogTable from cookbook.tables import CookLogTable, ViewLogTable
from cookbook.templatetags.theming_tags import get_theming_values from cookbook.templatetags.theming_tags import get_theming_values
from cookbook.version_info import VERSION_INFO from cookbook.version_info import VERSION_INFO
from recipes.settings import BASE_DIR, PLUGINS from recipes.settings import PLUGINS
def index(request): def index(request):
@@ -160,8 +157,8 @@ def recipe_view(request, pk, share=None):
servings = recipe.servings servings = recipe.servings
if request.method == "GET" and 'servings' in request.GET: if request.method == "GET" and 'servings' in request.GET:
servings = request.GET.get("servings") servings = request.GET.get("servings")
return render(request, 'recipe_view.html', return render(request, 'recipe_view.html', {'recipe': recipe, 'comments': comments, 'comment_form': comment_form, 'share': share, 'servings': servings})
{'recipe': recipe, 'comments': comments, 'comment_form': comment_form, 'share': share, 'servings': servings})
@group_required('user') @group_required('user')
@@ -174,16 +171,6 @@ def meal_plan(request):
return render(request, 'meal_plan.html', {}) return render(request, 'meal_plan.html', {})
@group_required('user')
def supermarket(request):
return render(request, 'supermarket.html', {})
@group_required('user')
def view_profile(request, user_id):
return render(request, 'profile.html', {})
@group_required('guest') @group_required('guest')
def user_settings(request): def user_settings(request):
if request.space.demo: if request.space.demo:
@@ -366,7 +353,8 @@ def setup(request):
if User.objects.count() > 0 or 'django.contrib.auth.backends.RemoteUserBackend' in settings.AUTHENTICATION_BACKENDS: if User.objects.count() > 0 or 'django.contrib.auth.backends.RemoteUserBackend' in settings.AUTHENTICATION_BACKENDS:
messages.add_message( messages.add_message(
request, messages.ERROR, request, messages.ERROR,
_('The setup page can only be used to create the first user! If you have forgotten your superuser credentials please consult the django documentation on how to reset passwords.' _('The setup page can only be used to create the first user! \
If you have forgotten your superuser credentials please consult the django documentation on how to reset passwords.'
)) ))
return HttpResponseRedirect(reverse('account_login')) return HttpResponseRedirect(reverse('account_login'))

View File

@@ -3,7 +3,7 @@ methods of central account management and authentication.
## Allauth ## Allauth
[Django Allauth](https://django-allauth.readthedocs.io/en/latest/index.html) is an awesome project that [Django Allauth](https://django-allauth.readthedocs.io/en/latest/index.html) is an awesome project that
allows you to use a [huge number](https://django-allauth.readthedocs.io/en/latest/providers.html) of different allows you to use a [huge number](https://docs.allauth.org/en/latest/socialaccount/providers/index.html) of different
authentication providers. authentication providers.
They basically explain everything in their documentation, but the following is a short overview on how to get started. They basically explain everything in their documentation, but the following is a short overview on how to get started.
@@ -17,42 +17,50 @@ They basically explain everything in their documentation, but the following is a
Choose a provider from the [list](https://docs.allauth.org/en/latest/socialaccount/providers/index.html) and install it using the environment variable `SOCIAL_PROVIDERS` as shown Choose a provider from the [list](https://docs.allauth.org/en/latest/socialaccount/providers/index.html) and install it using the environment variable `SOCIAL_PROVIDERS` as shown
in the example below. in the example below.
When at least one social provider is set up, the social login sign in buttons should appear on the login page. When at least one social provider is set up, the social login sign in buttons should appear on the login page. The example below enables Nextcloud and the generic OpenID Connect providers.
```ini ```ini
SOCIAL_PROVIDERS=allauth.socialaccount.providers.github,allauth.socialaccount.providers.nextcloud SOCIAL_PROVIDERS=allauth.socialaccount.providers.openid_connect,allauth.socialaccount.providers.nextcloud
``` ```
!!! warning "Formatting" !!! warning "Formatting"
The exact formatting is important so make sure to follow the steps explained here! The exact formatting is important so make sure to follow the steps explained here!
### Configuration, via environment
Depending on your authentication provider you **might need** to configure it. Depending on your authentication provider you **might need** to configure it.
This needs to be done through the settings system. To make the system flexible (allow multiple providers) and to This needs to be done through the settings system. To make the system flexible (allow multiple providers) and to
not require another file to be mounted into the container the configuration ins done through a single not require another file to be mounted into the container the configuration ins done through a single
environment variable. The downside of this approach is that the configuration needs to be put into a single line environment variable. The downside of this approach is that the configuration needs to be put into a single line
as environment files loaded by docker compose don't support multiple lines for a single variable. as environment files loaded by docker compose don't support multiple lines for a single variable.
The line data needs to either be in json or as Python dictionary syntax.
Take the example configuration from the allauth docs, fill in your settings and then inline the whole object Take the example configuration from the allauth docs, fill in your settings and then inline the whole object
(you can use a service like [www.freeformatter.com](https://www.freeformatter.com/json-formatter.html) for formatting). (you can use a service like [www.freeformatter.com](https://www.freeformatter.com/json-formatter.html) for formatting).
Assign it to the additional `SOCIALACCOUNT_PROVIDERS` variable. Assign it to the additional `SOCIALACCOUNT_PROVIDERS` variable.
The example below is for a generic OIDC provider with PKCE enabled. Most values need to be customized for your specifics!
```ini ```ini
SOCIALACCOUNT_PROVIDERS={"nextcloud":{"SERVER":"https://nextcloud.example.org"}} SOCIALACCOUNT_PROVIDERS = "{ 'openid_connect': { 'OAUTH_PKCE_ENABLED': True, 'APPS': [ { 'provider_id': 'oidc', 'name': 'My-IDM', 'client_id': 'my_client_id', 'secret': 'my_client_secret', 'settings': { 'server_url': 'https://idm.example.com/oidc/recipes' } } ] } }"
``` ```
!!! success "Improvements ?" !!! success "Improvements ?"
There are most likely ways to achieve the same goal but with a cleaner or simpler system. There are most likely ways to achieve the same goal but with a cleaner or simpler system.
If you know such a way feel free to let me know. If you know such a way feel free to let me know.
After that, use your superuser account to configure your authentication backend. ### Configuration, via Django Admin
Open the admin page and do the following
Instead of defining `SOCIALACCOUNT_PROVIDERS` in your environment, most configuration options can be done via the Admin interface. PKCE for `openid_connect` cannot currently be enabled this way.
Use your superuser account to configure your authentication backend by opening the admin page and do the following
1. Select `Sites` and edit the default site with the URL of your installation (or create a new). 1. Select `Sites` and edit the default site with the URL of your installation (or create a new).
2. Create a new `Social Application` with the required information as stated in the provider documentation of allauth. 2. Create a new `Social Application` with the required information as stated in the provider documentation of allauth.
3. Make sure to add your site to the list of available sites 3. Make sure to add your site to the list of available sites
Now the provider is configured and you should be able to sign up and sign in using the provider. Now the provider is configured and you should be able to sign up and sign in using the provider.
Use the superuser account to grant permissions to the newly created users. Use the superuser account to grant permissions to the newly created users, or enable default access via `SOCIAL_DEFAULT_ACCESS` & `SOCIAL_DEFAULT_GROUP`.
!!! info "WIP" !!! info "WIP"
I do not have a ton of experience with using various single signon providers and also cannot test all of them. I do not have a ton of experience with using various single signon providers and also cannot test all of them.
@@ -70,13 +78,7 @@ SOCIALACCOUNT_PROVIDERS='{"openid_connect":{"APPS":[{"provider_id":"keycloak","n
' '
``` ```
1. Restart the service, login as superuser and open the `Admin` page. You are now able to sign in using Keycloak after a restart of the service.
2. Make sure that the correct `Domain Name` is defined at `Sites`.
3. Select `Social Application` and chose `Keycloak` from the provider list.
4. Provide an arbitrary name for your authentication provider, and enter the `Client-ID` and `Secret Key` values obtained from Keycloak earlier.
5. Make sure to add your `Site` to the list of available sites and save the new `Social Application`.
You are now able to sign in using Keycloak.
### Linking accounts ### Linking accounts
To link an account to an already existing normal user go to the settings page of the user and link it. To link an account to an already existing normal user go to the settings page of the user and link it.

51
docs/install/archlinux.md Normal file
View File

@@ -0,0 +1,51 @@
!!! info "Community Contributed"
This guide was contributed by the community and is neither officially supported, nor updated or tested.
These are instructions for pacman based distributions, like ArchLinux. The package is available from the [AUR](https://aur.archlinux.org/packages/tandoor-recipes-git) or from [GitHub](https://github.com/jdecourval/tandoor-recipes-pkgbuild).
## Features
- systemd integration.
- Provide configuration for Nginx.
- Use socket activation.
- Use a non-root user.
- Apply migrations automatically.
## Installation
1. Clone the package, build and install with makepkg:
```shell
git clone https://aur.archlinux.org/tandoor-recipes-git.git
cd tandoor-recipes-git
makepkg -si
```
or use your favourite AUR helper.
2. Setup a PostgreSQL database and user, as explained here: https://docs.tandoor.dev/install/manual/#setup-postgresql
3. Configure the service in `/etc/tandoor/tandoor.conf`.
4. Reinstall the package, or follow [the official instructions](https://docs.tandoor.dev/install/manual/#initialize-the-application) to have tandoor creates its DB tables.
5. Optionally configure a reverse proxy. A configuration for Nginx is provided, but you can Traefik, Apache, etc..
Edit `/etc/nginx/sites-available/tandoor.conf`. You may want to use another `server_name`, or configure TLS. Then:
```shell
cd /etc/nginx/sites-enabled
ln -s ../sites-available/tandoor.conf
systemctl restart nginx
```
6. Enable the service
```shell
systemctl enable --now tandoor
```
## Upgrade
```shell
cd tandoor-recipes-git
git pull
makepkg -sif
```
Or use your favourite AUR helper.
You shouldn't need to do anything else. This package applies migration automatically. If PostgreSQL has been updated to a new major version, you may need to [run pg_upgrade](https://wiki.archlinux.org/title/PostgreSQL#pg_upgrade).
## Help
This package is non-official. Issues should be posted to https://github.com/jdecourval/tandoor-recipes-pkgbuild or https://aur.archlinux.org/packages/tandoor-recipes-git.

View File

@@ -35,6 +35,7 @@ nav:
- KubeSail or PiBox: install/kubesail.md - KubeSail or PiBox: install/kubesail.md
- TrueNAS Portainer: install/truenas_portainer.md - TrueNAS Portainer: install/truenas_portainer.md
- WSL: install/wsl.md - WSL: install/wsl.md
- ArchLinux: install/archlinux.md
- Manual: install/manual.md - Manual: install/manual.md
- Other setups: install/other.md - Other setups: install/other.md
- Features: - Features:

View File

@@ -26,8 +26,7 @@ load_dotenv()
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Get vars from .env files # Get vars from .env files
SECRET_KEY = os.getenv('SECRET_KEY') if os.getenv( SECRET_KEY = os.getenv('SECRET_KEY') if os.getenv('SECRET_KEY') else 'INSECURE_STANDARD_KEY_SET_IN_ENV'
'SECRET_KEY') else 'INSECURE_STANDARD_KEY_SET_IN_ENV'
DEBUG = bool(int(os.getenv('DEBUG', True))) DEBUG = bool(int(os.getenv('DEBUG', True)))
DEBUG_TOOLBAR = bool(int(os.getenv('DEBUG_TOOLBAR', True))) DEBUG_TOOLBAR = bool(int(os.getenv('DEBUG_TOOLBAR', True)))
@@ -38,11 +37,9 @@ SOCIAL_DEFAULT_GROUP = os.getenv('SOCIAL_DEFAULT_GROUP', 'guest')
SPACE_DEFAULT_MAX_RECIPES = int(os.getenv('SPACE_DEFAULT_MAX_RECIPES', 0)) SPACE_DEFAULT_MAX_RECIPES = int(os.getenv('SPACE_DEFAULT_MAX_RECIPES', 0))
SPACE_DEFAULT_MAX_USERS = int(os.getenv('SPACE_DEFAULT_MAX_USERS', 0)) SPACE_DEFAULT_MAX_USERS = int(os.getenv('SPACE_DEFAULT_MAX_USERS', 0))
SPACE_DEFAULT_MAX_FILES = int(os.getenv('SPACE_DEFAULT_MAX_FILES', 0)) SPACE_DEFAULT_MAX_FILES = int(os.getenv('SPACE_DEFAULT_MAX_FILES', 0))
SPACE_DEFAULT_ALLOW_SHARING = bool( SPACE_DEFAULT_ALLOW_SHARING = bool(int(os.getenv('SPACE_DEFAULT_ALLOW_SHARING', True)))
int(os.getenv('SPACE_DEFAULT_ALLOW_SHARING', True)))
INTERNAL_IPS = os.getenv('INTERNAL_IPS').split( INTERNAL_IPS = os.getenv('INTERNAL_IPS').split(',') if os.getenv('INTERNAL_IPS') else ['127.0.0.1']
',') if os.getenv('INTERNAL_IPS') else ['127.0.0.1']
# allow djangos wsgi server to server mediafiles # allow djangos wsgi server to server mediafiles
GUNICORN_MEDIA = bool(int(os.getenv('GUNICORN_MEDIA', False))) GUNICORN_MEDIA = bool(int(os.getenv('GUNICORN_MEDIA', False)))
@@ -63,18 +60,15 @@ UNAUTHENTICATED_THEME_FROM_SPACE = int(os.getenv('UNAUTHENTICATED_THEME_FROM_SPA
FORCE_THEME_FROM_SPACE = int(os.getenv('FORCE_THEME_FROM_SPACE', 0)) FORCE_THEME_FROM_SPACE = int(os.getenv('FORCE_THEME_FROM_SPACE', 0))
# minimum interval that users can set for automatic sync of shopping lists # minimum interval that users can set for automatic sync of shopping lists
SHOPPING_MIN_AUTOSYNC_INTERVAL = int( SHOPPING_MIN_AUTOSYNC_INTERVAL = int(os.getenv('SHOPPING_MIN_AUTOSYNC_INTERVAL', 5))
os.getenv('SHOPPING_MIN_AUTOSYNC_INTERVAL', 5))
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS').split( ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS').split(',') if os.getenv('ALLOWED_HOSTS') else ['*']
',') if os.getenv('ALLOWED_HOSTS') else ['*']
if os.getenv('CSRF_TRUSTED_ORIGINS'): if os.getenv('CSRF_TRUSTED_ORIGINS'):
CSRF_TRUSTED_ORIGINS = os.getenv('CSRF_TRUSTED_ORIGINS').split(',') CSRF_TRUSTED_ORIGINS = os.getenv('CSRF_TRUSTED_ORIGINS').split(',')
if CORS_ORIGIN_ALLOW_ALL := os.getenv('CORS_ORIGIN_ALLOW_ALL') is not None: if CORS_ORIGIN_ALLOW_ALL := os.getenv('CORS_ORIGIN_ALLOW_ALL') is not None:
print( print('DEPRECATION WARNING: Environment var "CORS_ORIGIN_ALLOW_ALL" is deprecated. Please use "CORS_ALLOW_ALL_ORIGINS."')
'DEPRECATION WARNING: Environment var "CORS_ORIGIN_ALLOW_ALL" is deprecated. Please use "CORS_ALLOW_ALL_ORIGINS."')
CORS_ALLOW_ALL_ORIGINS = CORS_ORIGIN_ALLOW_ALL CORS_ALLOW_ALL_ORIGINS = CORS_ORIGIN_ALLOW_ALL
else: else:
CORS_ALLOW_ALL_ORIGINS = bool(int(os.getenv("CORS_ALLOW_ALL_ORIGINS", True))) CORS_ALLOW_ALL_ORIGINS = bool(int(os.getenv("CORS_ALLOW_ALL_ORIGINS", True)))
@@ -106,9 +100,7 @@ PRIVACY_URL = os.getenv('PRIVACY_URL', '')
IMPRINT_URL = os.getenv('IMPRINT_URL', '') IMPRINT_URL = os.getenv('IMPRINT_URL', '')
HOSTED = bool(int(os.getenv('HOSTED', False))) HOSTED = bool(int(os.getenv('HOSTED', False)))
MESSAGE_TAGS = { MESSAGE_TAGS = {messages.ERROR: 'danger'}
messages.ERROR: 'danger'
}
# Application definition # Application definition
@@ -162,8 +154,7 @@ try:
INSTALLED_APPS.append(plugin_module) INSTALLED_APPS.append(plugin_module)
plugin_config = { plugin_config = {
'name': plugin_class.verbose_name if hasattr(plugin_class, 'name': plugin_class.verbose_name if hasattr(plugin_class, 'verbose_name') else plugin_class.name,
'verbose_name') else plugin_class.name,
'version': plugin_class.VERSION if hasattr(plugin_class, 'VERSION') else 'unknown', 'version': plugin_class.VERSION if hasattr(plugin_class, 'VERSION') else 'unknown',
'website': plugin_class.website if hasattr(plugin_class, 'website') else '', 'website': plugin_class.website if hasattr(plugin_class, 'website') else '',
'github': plugin_class.github if hasattr(plugin_class, 'github') else '', 'github': plugin_class.github if hasattr(plugin_class, 'github') else '',
@@ -171,8 +162,7 @@ try:
'base_path': os.path.join(BASE_DIR, 'recipes', 'plugins', d), 'base_path': os.path.join(BASE_DIR, 'recipes', 'plugins', d),
'base_url': plugin_class.base_url, 'base_url': plugin_class.base_url,
'bundle_name': plugin_class.bundle_name if hasattr(plugin_class, 'bundle_name') else '', 'bundle_name': plugin_class.bundle_name if hasattr(plugin_class, 'bundle_name') else '',
'api_router_name': plugin_class.api_router_name if hasattr(plugin_class, 'api_router_name': plugin_class.api_router_name if hasattr(plugin_class, 'api_router_name') else '',
'api_router_name') else '',
'nav_main': plugin_class.nav_main if hasattr(plugin_class, 'nav_main') else '', 'nav_main': plugin_class.nav_main if hasattr(plugin_class, 'nav_main') else '',
'nav_dropdown': plugin_class.nav_dropdown if hasattr(plugin_class, 'nav_dropdown') else '', 'nav_dropdown': plugin_class.nav_dropdown if hasattr(plugin_class, 'nav_dropdown') else '',
} }
@@ -186,8 +176,7 @@ except Exception:
if DEBUG: if DEBUG:
print('ERROR failed to initialize plugins') print('ERROR failed to initialize plugins')
SOCIAL_PROVIDERS = os.getenv('SOCIAL_PROVIDERS').split( SOCIAL_PROVIDERS = os.getenv('SOCIAL_PROVIDERS').split(',') if os.getenv('SOCIAL_PROVIDERS') else []
',') if os.getenv('SOCIAL_PROVIDERS') else []
SOCIALACCOUNT_EMAIL_VERIFICATION = 'none' SOCIALACCOUNT_EMAIL_VERIFICATION = 'none'
INSTALLED_APPS = INSTALLED_APPS + SOCIAL_PROVIDERS INSTALLED_APPS = INSTALLED_APPS + SOCIAL_PROVIDERS
@@ -198,11 +187,9 @@ ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = 90
ACCOUNT_LOGOUT_ON_GET = True ACCOUNT_LOGOUT_ON_GET = True
try: try:
SOCIALACCOUNT_PROVIDERS = ast.literal_eval( SOCIALACCOUNT_PROVIDERS = ast.literal_eval(os.getenv('SOCIALACCOUNT_PROVIDERS') if os.getenv('SOCIALACCOUNT_PROVIDERS') else '{}')
os.getenv('SOCIALACCOUNT_PROVIDERS') if os.getenv('SOCIALACCOUNT_PROVIDERS') else '{}')
except ValueError: except ValueError:
SOCIALACCOUNT_PROVIDERS = json.loads( SOCIALACCOUNT_PROVIDERS = json.loads(os.getenv('SOCIALACCOUNT_PROVIDERS').replace("'", '"') if os.getenv('SOCIALACCOUNT_PROVIDERS') else '{}')
os.getenv('SOCIALACCOUNT_PROVIDERS').replace("'", '"') if os.getenv('SOCIALACCOUNT_PROVIDERS') else '{}')
SESSION_COOKIE_DOMAIN = os.getenv('SESSION_COOKIE_DOMAIN', None) SESSION_COOKIE_DOMAIN = os.getenv('SESSION_COOKIE_DOMAIN', None)
SESSION_COOKIE_NAME = os.getenv('SESSION_COOKIE_NAME', 'sessionid') SESSION_COOKIE_NAME = os.getenv('SESSION_COOKIE_NAME', 'sessionid')
@@ -215,30 +202,21 @@ ENABLE_PDF_EXPORT = bool(int(os.getenv('ENABLE_PDF_EXPORT', False)))
EXPORT_FILE_CACHE_DURATION = int(os.getenv('EXPORT_FILE_CACHE_DURATION', 600)) EXPORT_FILE_CACHE_DURATION = int(os.getenv('EXPORT_FILE_CACHE_DURATION', 600))
MIDDLEWARE = [ MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', 'corsheaders.middleware.CorsMiddleware', 'django.middleware.security.SecurityMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware',
'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.locale.LocaleMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'cookbook.helper.scope_middleware.ScopeMiddleware', 'allauth.account.middleware.AccountMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'cookbook.helper.scope_middleware.ScopeMiddleware',
'allauth.account.middleware.AccountMiddleware',
] ]
if DEBUG_TOOLBAR: if DEBUG_TOOLBAR:
MIDDLEWARE += ('debug_toolbar.middleware.DebugToolbarMiddleware',) MIDDLEWARE += ('debug_toolbar.middleware.DebugToolbarMiddleware', )
INSTALLED_APPS += ('debug_toolbar',) INSTALLED_APPS += ('debug_toolbar', )
SORT_TREE_BY_NAME = bool(int(os.getenv('SORT_TREE_BY_NAME', False))) SORT_TREE_BY_NAME = bool(int(os.getenv('SORT_TREE_BY_NAME', False)))
DISABLE_TREE_FIX_STARTUP = bool( DISABLE_TREE_FIX_STARTUP = bool(int(os.getenv('DISABLE_TREE_FIX_STARTUP', False)))
int(os.getenv('DISABLE_TREE_FIX_STARTUP', False)))
if bool(int(os.getenv('SQL_DEBUG', False))): if bool(int(os.getenv('SQL_DEBUG', False))):
MIDDLEWARE += ('recipes.middleware.SqlPrintingMiddleware',) MIDDLEWARE += ('recipes.middleware.SqlPrintingMiddleware', )
if ENABLE_METRICS: if ENABLE_METRICS:
MIDDLEWARE += 'django_prometheus.middleware.PrometheusAfterMiddleware', MIDDLEWARE += 'django_prometheus.middleware.PrometheusAfterMiddleware',
@@ -257,35 +235,34 @@ if LDAP_AUTH:
AUTH_LDAP_START_TLS = bool(int(os.getenv('AUTH_LDAP_START_TLS', False))) AUTH_LDAP_START_TLS = bool(int(os.getenv('AUTH_LDAP_START_TLS', False)))
AUTH_LDAP_BIND_DN = os.getenv('AUTH_LDAP_BIND_DN') AUTH_LDAP_BIND_DN = os.getenv('AUTH_LDAP_BIND_DN')
AUTH_LDAP_BIND_PASSWORD = os.getenv('AUTH_LDAP_BIND_PASSWORD') AUTH_LDAP_BIND_PASSWORD = os.getenv('AUTH_LDAP_BIND_PASSWORD')
AUTH_LDAP_USER_SEARCH = LDAPSearch( AUTH_LDAP_USER_SEARCH = LDAPSearch(os.getenv('AUTH_LDAP_USER_SEARCH_BASE_DN'), ldap.SCOPE_SUBTREE, os.getenv('AUTH_LDAP_USER_SEARCH_FILTER_STR', '(uid=%(user)s)'), )
os.getenv('AUTH_LDAP_USER_SEARCH_BASE_DN'), AUTH_LDAP_USER_ATTR_MAP = ast.literal_eval(os.getenv('AUTH_LDAP_USER_ATTR_MAP')) if os.getenv('AUTH_LDAP_USER_ATTR_MAP') else {
ldap.SCOPE_SUBTREE,
os.getenv('AUTH_LDAP_USER_SEARCH_FILTER_STR', '(uid=%(user)s)'),
)
AUTH_LDAP_USER_ATTR_MAP = ast.literal_eval(os.getenv('AUTH_LDAP_USER_ATTR_MAP')) if os.getenv(
'AUTH_LDAP_USER_ATTR_MAP') else {
'first_name': 'givenName', 'first_name': 'givenName',
'last_name': 'sn', 'last_name': 'sn',
'email': 'mail', 'email': 'mail',
} }
AUTH_LDAP_ALWAYS_UPDATE_USER = bool( AUTH_LDAP_ALWAYS_UPDATE_USER = bool(int(os.getenv('AUTH_LDAP_ALWAYS_UPDATE_USER', True)))
int(os.getenv('AUTH_LDAP_ALWAYS_UPDATE_USER', True)))
AUTH_LDAP_CACHE_TIMEOUT = int(os.getenv('AUTH_LDAP_CACHE_TIMEOUT', 3600)) AUTH_LDAP_CACHE_TIMEOUT = int(os.getenv('AUTH_LDAP_CACHE_TIMEOUT', 3600))
if 'AUTH_LDAP_TLS_CACERTFILE' in os.environ: if 'AUTH_LDAP_TLS_CACERTFILE' in os.environ:
AUTH_LDAP_GLOBAL_OPTIONS = { AUTH_LDAP_GLOBAL_OPTIONS = {ldap.OPT_X_TLS_CACERTFILE: os.getenv('AUTH_LDAP_TLS_CACERTFILE')}
ldap.OPT_X_TLS_CACERTFILE: os.getenv('AUTH_LDAP_TLS_CACERTFILE')}
if DEBUG: if DEBUG:
LOGGING = { LOGGING = {
"version": 1, "version": 1,
"disable_existing_loggers": False, "disable_existing_loggers": False,
"handlers": {"console": {"class": "logging.StreamHandler"}}, "handlers": {
"loggers": {"django_auth_ldap": {"level": "DEBUG", "handlers": ["console"]}}, "console": {
"class": "logging.StreamHandler"
}
},
"loggers": {
"django_auth_ldap": {
"level": "DEBUG",
"handlers": ["console"]
}
},
} }
AUTHENTICATION_BACKENDS += [ AUTHENTICATION_BACKENDS += ['django.contrib.auth.backends.ModelBackend', 'allauth.account.auth_backends.AuthenticationBackend', ]
'django.contrib.auth.backends.ModelBackend',
'allauth.account.auth_backends.AuthenticationBackend',
]
# django allauth site id # django allauth site id
SITE_ID = int(os.getenv('ALLAUTH_SITE_ID', 1)) SITE_ID = int(os.getenv('ALLAUTH_SITE_ID', 1))
@@ -294,75 +271,55 @@ ACCOUNT_ADAPTER = 'cookbook.helper.AllAuthCustomAdapter'
if REMOTE_USER_AUTH: if REMOTE_USER_AUTH:
MIDDLEWARE.insert(8, 'recipes.middleware.CustomRemoteUser') MIDDLEWARE.insert(8, 'recipes.middleware.CustomRemoteUser')
AUTHENTICATION_BACKENDS.append( AUTHENTICATION_BACKENDS.append('django.contrib.auth.backends.RemoteUserBackend')
'django.contrib.auth.backends.RemoteUserBackend')
# Password validation # Password validation
# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [ AUTH_PASSWORD_VALIDATORS = [{
{ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, {
}, 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
{ }, {
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
}, }, {
{ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, ]
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
OAUTH2_PROVIDER = { OAUTH2_PROVIDER = {'SCOPES': {'read': 'Read scope', 'write': 'Write scope', 'bookmarklet': 'only access to bookmarklet'}}
'SCOPES': {'read': 'Read scope', 'write': 'Write scope', 'bookmarklet': 'only access to bookmarklet'}
}
READ_SCOPE = 'read' READ_SCOPE = 'read'
WRITE_SCOPE = 'write' WRITE_SCOPE = 'write'
REST_FRAMEWORK = { REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': ( 'DEFAULT_AUTHENTICATION_CLASSES':
'rest_framework.authentication.SessionAuthentication', ('rest_framework.authentication.SessionAuthentication', 'oauth2_provider.contrib.rest_framework.OAuth2Authentication', 'rest_framework.authentication.BasicAuthentication',
'oauth2_provider.contrib.rest_framework.OAuth2Authentication', ),
'rest_framework.authentication.BasicAuthentication', 'DEFAULT_PERMISSION_CLASSES': ['rest_framework.permissions.IsAuthenticated', ],
),
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
} }
ROOT_URLCONF = 'recipes.urls' ROOT_URLCONF = 'recipes.urls'
TEMPLATES = [ TEMPLATES = [{
{ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(BASE_DIR, 'templates'), os.path.join(BASE_DIR, 'cookbook', 'templates')],
'DIRS': [os.path.join(BASE_DIR, 'templates'), os.path.join(BASE_DIR, 'cookbook', 'templates')], 'APP_DIRS': True,
'APP_DIRS': True, 'OPTIONS': {
'OPTIONS': { 'context_processors': [
'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth',
'django.template.context_processors.debug', 'django.contrib.messages.context_processors.messages', 'django.template.context_processors.media', 'cookbook.helper.context_processors.context_settings',
'django.template.context_processors.request', ],
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'django.template.context_processors.media',
'cookbook.helper.context_processors.context_settings',
],
},
}, },
] }, ]
WSGI_APPLICATION = 'recipes.wsgi.application' WSGI_APPLICATION = 'recipes.wsgi.application'
# Database # Database
# Load settings from env files # Load settings from env files
if os.getenv('DATABASE_URL'): if os.getenv('DATABASE_URL'):
match = re.match( match = re.match(r'(?P<schema>\w+):\/\/(?:(?P<user>[\w\d_-]+)(?::(?P<password>[^@]+))?@)?(?P<host>[^:/]+)(?::(?P<port>\d+))?(?:/(?P<database>[\w\d/._-]+))?',
r'(?P<schema>\w+):\/\/(?:(?P<user>[\w\d_-]+)(?::(?P<password>[^@]+))?@)?(?P<host>[^:/]+)(?::(?P<port>\d+))?(?:/(?P<database>[\w\d/._-]+))?', os.getenv('DATABASE_URL'))
os.getenv('DATABASE_URL')
)
settings = match.groupdict() settings = match.groupdict()
schema = settings['schema'] schema = settings['schema']
if schema.startswith('postgres'): if schema.startswith('postgres'):
@@ -423,12 +380,7 @@ else:
# } # }
# } # }
CACHES = { CACHES = {'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 'LOCATION': 'default', }}
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'default',
}
}
# Vue webpack settings # Vue webpack settings
VUE_DIR = os.path.join(BASE_DIR, 'vue') VUE_DIR = os.path.join(BASE_DIR, 'vue')
@@ -491,33 +443,16 @@ USE_L10N = True
USE_TZ = True USE_TZ = True
LANGUAGES = [ LANGUAGES = [('hy', _('Armenian ')), ('bg', _('Bulgarian')), ('ca', _('Catalan')), ('cs', _('Czech')), ('da', _('Danish')), ('nl', _('Dutch')), ('en', _('English')),
('hy', _('Armenian ')), ('fr', _('French')), ('de', _('German')), ('hu', _('Hungarian')), ('it', _('Italian')), ('lv', _('Latvian')), ('nb', _('Norwegian ')), ('pl', _('Polish')),
('bg', _('Bulgarian')), ('ru', _('Russian')), ('es', _('Spanish')), ('sv', _('Swedish')), ]
('ca', _('Catalan')),
('cs', _('Czech')),
('da', _('Danish')),
('nl', _('Dutch')),
('en', _('English')),
('fr', _('French')),
('de', _('German')),
('hu', _('Hungarian')),
('it', _('Italian')),
('lv', _('Latvian')),
('nb', _('Norwegian ')),
('pl', _('Polish')),
('ru', _('Russian')),
('es', _('Spanish')),
('sv', _('Swedish')),
]
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.0/howto/static-files/ # https://docs.djangoproject.com/en/2.0/howto/static-files/
SCRIPT_NAME = os.getenv('SCRIPT_NAME', '') SCRIPT_NAME = os.getenv('SCRIPT_NAME', '')
# path for django_js_reverse to generate the javascript file containing all urls. Only done because the default command (collectstatic_js_reverse) fails to update the manifest # path for django_js_reverse to generate the javascript file containing all urls. Only done because the default command (collectstatic_js_reverse) fails to update the manifest
JS_REVERSE_OUTPUT_PATH = os.path.join( JS_REVERSE_OUTPUT_PATH = os.path.join(BASE_DIR, "cookbook/static/django_js_reverse")
BASE_DIR, "cookbook/static/django_js_reverse")
JS_REVERSE_SCRIPT_PREFIX = os.getenv('JS_REVERSE_SCRIPT_PREFIX', SCRIPT_NAME) JS_REVERSE_SCRIPT_PREFIX = os.getenv('JS_REVERSE_SCRIPT_PREFIX', SCRIPT_NAME)
STATIC_URL = os.getenv('STATIC_URL', '/static/') STATIC_URL = os.getenv('STATIC_URL', '/static/')
@@ -572,23 +507,13 @@ EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', '')
EMAIL_USE_TLS = bool(int(os.getenv('EMAIL_USE_TLS', False))) EMAIL_USE_TLS = bool(int(os.getenv('EMAIL_USE_TLS', False)))
EMAIL_USE_SSL = bool(int(os.getenv('EMAIL_USE_SSL', False))) EMAIL_USE_SSL = bool(int(os.getenv('EMAIL_USE_SSL', False)))
DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL', 'webmaster@localhost') DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL', 'webmaster@localhost')
ACCOUNT_EMAIL_SUBJECT_PREFIX = os.getenv( ACCOUNT_EMAIL_SUBJECT_PREFIX = os.getenv('ACCOUNT_EMAIL_SUBJECT_PREFIX', '[Tandoor Recipes] ') # allauth sender prefix
'ACCOUNT_EMAIL_SUBJECT_PREFIX', '[Tandoor Recipes] ') # allauth sender prefix
# ACCOUNT_SIGNUP_FORM_CLASS = 'cookbook.forms.AllAuthSignupForm' # ACCOUNT_SIGNUP_FORM_CLASS = 'cookbook.forms.AllAuthSignupForm'
ACCOUNT_FORMS = { ACCOUNT_FORMS = {'signup': 'cookbook.forms.AllAuthSignupForm', 'reset_password': 'cookbook.forms.CustomPasswordResetForm'}
'signup': 'cookbook.forms.AllAuthSignupForm',
'reset_password': 'cookbook.forms.CustomPasswordResetForm'
}
ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS = False ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS = False
ACCOUNT_RATE_LIMITS = { ACCOUNT_RATE_LIMITS = {"change_password": "1/m/user", "reset_password": "1/m/ip,1/m/key", "reset_password_from_key": "1/m/ip", "signup": "5/m/ip", "login": "5/m/ip", }
"change_password": "1/m/user",
"reset_password": "1/m/ip,1/m/key",
"reset_password_from_key": "1/m/ip",
"signup": "5/m/ip",
"login": "5/m/ip",
}
DISABLE_EXTERNAL_CONNECTORS = bool(int(os.getenv('DISABLE_EXTERNAL_CONNECTORS', False))) DISABLE_EXTERNAL_CONNECTORS = bool(int(os.getenv('DISABLE_EXTERNAL_CONNECTORS', False)))
EXTERNAL_CONNECTORS_QUEUE_SIZE = int(os.getenv('EXTERNAL_CONNECTORS_QUEUE_SIZE', 100)) EXTERNAL_CONNECTORS_QUEUE_SIZE = int(os.getenv('EXTERNAL_CONNECTORS_QUEUE_SIZE', 100))

View File

@@ -1,7 +1,6 @@
Django==4.2.10 Django==4.2.10
cryptography===42.0.4 cryptography===42.0.4
django-annoying==0.10.6 django-annoying==0.10.6
django-autocomplete-light==3.9.7
django-cleanup==8.0.0 django-cleanup==8.0.0
django-crispy-forms==2.0 django-crispy-forms==2.0
crispy-bootstrap4==2022.1 crispy-bootstrap4==2022.1

View File

@@ -1,177 +0,0 @@
<template>
<!-- TODO: Deprecate -->
<div id="app">
<div class="row">
<div class="col col-md-12">
<h2>{{ $t("Supermarket") }}</h2>
<multiselect v-model="selected_supermarket" track-by="id" label="name" :options="supermarkets" @input="selectedSupermarketChanged"> </multiselect>
<b-button class="btn btn-primary btn-block" style="margin-top: 1vh" v-b-modal.modal-supermarket>
{{ $t("Edit") }}
</b-button>
<b-button class="btn btn-success btn-block" @click="selected_supermarket = { new: true, name: '' }" v-b-modal.modal-supermarket>{{ $t("New") }} </b-button>
</div>
</div>
<hr />
<div class="row">
<div class="col col-md-6">
<h4>
{{ $t("Categories") }}
<button class="btn btn-success btn-sm" @click="selected_category = { new: true, name: '' }" v-b-modal.modal-category>{{ $t("New") }}</button>
</h4>
<draggable :list="selectable_categories" group="supermarket_categories" :empty-insert-threshold="10">
<div v-for="c in selectable_categories" :key="c.id">
<button class="btn btn-block btn-sm btn-primary" style="margin-top: 0.5vh">{{ c.name }}</button>
</div>
</draggable>
</div>
<div class="col col-md-6">
<h4>{{ $t("Selected") }} {{ $t("Categories") }}</h4>
<draggable :list="supermarket_categories" group="supermarket_categories" :empty-insert-threshold="10" @change="selectedCategoriesChanged">
<div v-for="c in supermarket_categories" :key="c.id">
<button class="btn btn-block btn-sm btn-primary" style="margin-top: 0.5vh">{{ c.name }}</button>
</div>
</draggable>
</div>
</div>
<!-- EDIT MODALS -->
<b-modal id="modal-supermarket" v-bind:title="$t('Supermarket')" @ok="supermarketModalOk()">
<label v-if="selected_supermarket !== undefined">
{{ $t("Name") }}
<b-input v-model="selected_supermarket.name"></b-input>
</label>
</b-modal>
<b-modal id="modal-category" v-bind:title="$t('Category')" @ok="categoryModalOk()">
<label v-if="selected_category !== undefined">
{{ $t("Name") }}
<b-input v-model="selected_category.name"></b-input>
</label>
</b-modal>
</div>
</template>
<script>
import Vue from "vue"
import { BootstrapVue } from "bootstrap-vue"
import "bootstrap-vue/dist/bootstrap-vue.css"
import { ResolveUrlMixin, ToastMixin } from "@/utils/utils"
import { ApiApiFactory } from "@/utils/openapi/api.ts"
Vue.use(BootstrapVue)
import draggable from "vuedraggable"
import axios from "axios"
import Multiselect from "vue-multiselect"
axios.defaults.xsrfHeaderName = "X-CSRFToken"
axios.defaults.xsrfCookieName = "csrftoken"
export default {
name: "SupermarketView",
mixins: [ResolveUrlMixin, ToastMixin],
components: {
Multiselect,
draggable,
},
data() {
return {
supermarkets: [],
categories: [],
selected_supermarket: {},
selected_category: {},
selectable_categories: [],
supermarket_categories: [],
}
},
mounted() {
this.$i18n.locale = window.CUSTOM_LOCALE
this.loadInitial()
},
methods: {
loadInitial: function () {
let apiClient = new ApiApiFactory()
apiClient.listSupermarkets().then((results) => {
this.supermarkets = results.data
})
apiClient.listSupermarketCategorys().then((results) => {
this.categories = results.data
this.selectable_categories = this.categories
})
},
selectedCategoriesChanged: function (data) {
let apiClient = new ApiApiFactory()
if ("removed" in data) {
let relation = this.selected_supermarket.category_to_supermarket.filter((el) => el.category.id === data.removed.element.id)[0]
apiClient.destroySupermarketCategoryRelation(relation.id)
}
if ("added" in data) {
apiClient
.createSupermarketCategoryRelation({
category: data.added.element,
supermarket: this.selected_supermarket.id,
order: 0,
})
.then((results) => {
this.selected_supermarket.category_to_supermarket.push(results.data)
})
}
if ("moved" in data || "added" in data) {
this.supermarket_categories.forEach((element, index) => {
let relation = this.selected_supermarket.category_to_supermarket.filter((el) => el.category.id === element.id)[0]
apiClient.partialUpdateSupermarketCategoryRelation(relation.id, { order: index })
})
}
},
selectedSupermarketChanged: function (supermarket, id) {
this.supermarket_categories = []
this.selectable_categories = this.categories
for (let i of supermarket.category_to_supermarket) {
this.supermarket_categories.push(i.category)
this.selectable_categories = this.selectable_categories.filter(function (el) {
return el.id !== i.category.id
})
}
},
supermarketModalOk: function () {
let apiClient = new ApiApiFactory()
if (this.selected_supermarket.new) {
apiClient.createSupermarket({ name: this.selected_supermarket.name }).then((results) => {
this.selected_supermarket = undefined
this.loadInitial()
})
} else {
apiClient.partialUpdateSupermarket(this.selected_supermarket.id, { name: this.selected_supermarket.name })
}
},
categoryModalOk: function () {
let apiClient = new ApiApiFactory()
if (this.selected_category.new) {
apiClient.createSupermarketCategory({ name: this.selected_category.name }).then((results) => {
this.selected_category = {}
this.loadInitial()
})
} else {
apiClient.partialUpdateSupermarketCategory(this.selected_category.id, { name: this.selected_category.name })
}
},
},
}
</script>
<style></style>

View File

@@ -1,22 +0,0 @@
import Vue from 'vue'
import App from './SupermarketView.vue'
import i18n from '@/i18n'
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
// TODO move this and other default stuff to centralized JS file (verify nothing breaks)
let publicPath = localStorage.STATIC_URL + 'vue/'
if (process.env.NODE_ENV === 'development') {
publicPath = 'http://localhost:8080/'
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

View File

@@ -200,7 +200,7 @@
"Keyword_Alias": "Nyckelord alias", "Keyword_Alias": "Nyckelord alias",
"Recipe_Book": "Receptbok", "Recipe_Book": "Receptbok",
"Search Settings": "Sökinställningar", "Search Settings": "Sökinställningar",
"warning_feature_beta": "Den här funktionen är för närvarande i ett BETA-läge (testning). Vänligen förvänta dig buggar och eventuellt brytande ändringar i framtiden (möjligen att förlora funktionsrelaterad data) när du använder den här funktionen.", "warning_feature_beta": "Den här funktionen är för närvarande i ett BETA-läge (testning). Förvänta dig buggar och eventuellt större ändringar i framtiden (möjligtvis framtida data kan gå förlorad) när du använder den här funktionen.",
"success_deleting_resource": "En resurs har raderats!", "success_deleting_resource": "En resurs har raderats!",
"file_upload_disabled": "Filuppladdning är inte aktiverat för ditt utrymme.", "file_upload_disabled": "Filuppladdning är inte aktiverat för ditt utrymme.",
"show_only_internal": "Visa endast interna recept", "show_only_internal": "Visa endast interna recept",
@@ -246,7 +246,7 @@
"CountMore": "...+{count} fler", "CountMore": "...+{count} fler",
"IgnoreThis": "Lägg aldrig till {mat} automatiskt i inköpslista", "IgnoreThis": "Lägg aldrig till {mat} automatiskt i inköpslista",
"DelayFor": "Fördröjning på {hours} timmar", "DelayFor": "Fördröjning på {hours} timmar",
"ShowDelayed": "Visa fördröjda artiklar", "ShowDelayed": "Visa fördröjda föremål",
"Completed": "Avslutad", "Completed": "Avslutad",
"OfflineAlert": "Du är offline, inköpslistan kanske inte synkroniseras.", "OfflineAlert": "Du är offline, inköpslistan kanske inte synkroniseras.",
"shopping_share": "Dela inköpslista", "shopping_share": "Dela inköpslista",
@@ -477,5 +477,64 @@
"Warning_Delete_Supermarket_Category": "Om du tar bort en mataffärskategori raderas också alla relationer till livsmedel. Är du säker?", "Warning_Delete_Supermarket_Category": "Om du tar bort en mataffärskategori raderas också alla relationer till livsmedel. Är du säker?",
"Disabled": "Inaktiverad", "Disabled": "Inaktiverad",
"Social_Authentication": "Social autentisering", "Social_Authentication": "Social autentisering",
"Single": "Enstaka" "Single": "Enstaka",
"Properties": "Egenskaper",
"err_importing_recipe": "Ett fel uppstod vid import av receptet!",
"recipe_property_info": "Du kan också lägga till egenskaper till maträtter för att beräkna dessa automatiskt baserat på ditt recept!",
"total": "totalt",
"CustomLogos": "Anpassade logotyper",
"Welcome": "Välkommen",
"Input": "Inmatning",
"Undo": "Ångra",
"NoMoreUndo": "Inga ändringar att ångra.",
"Delete_All": "Radera alla",
"Property": "Egendom",
"Property_Editor": "Egendom redigerare",
"Conversion": "Omvandling",
"created_by": "Skapad av",
"ShowRecentlyCompleted": "Visa nyligen genomförda föremål",
"ShoppingBackgroundSyncWarning": "Dålig uppkoppling, inväntar synkronisering...",
"show_step_ingredients": "Visa ingredienser för steget",
"hide_step_ingredients": "Dölj ingredienser för steget",
"Logo": "Logga",
"Show_Logo": "Visa logga",
"Show_Logo_Help": "Visa Tandoor eller hushålls-logga i navigationen.",
"Nav_Text_Mode": "Navigation Textläge",
"Nav_Text_Mode_Help": "Beter sig annorlunda för varje tema.",
"g": "gram [g] (metriskt, vikt)",
"kg": "kilogram [kg] (metriskt, vikt)",
"ounce": "ounce [oz] (vikt)",
"FDC_Search": "FDC Sök",
"property_type_fdc_hint": "Bara egendomstyper med ett FDC ID kan automatiskt hämta data från FDC databasen",
"Alignment": "Orientering",
"base_amount": "Basmängd",
"Datatype": "Datatyp",
"Number of Objects": "Antal objekt",
"StartDate": "Startdatum",
"EndDate": "Slutdatum",
"FDC_ID_help": "FDC databas ID",
"Data_Import_Info": "Förbättra din samling genom att importera en framtagen lista av livsmedel, enheter och mer för att förbättra din recept-samling.",
"Update_Existing_Data": "Uppdatera existerande data",
"Use_Metric": "Använd metriska enheter",
"Learn_More": "Läs mer",
"converted_unit": "Konverterad enhet",
"converted_amount": "Konverterad mängd",
"base_unit": "Basenhet",
"FDC_ID": "FDC ID",
"per_serving": "per servering",
"Properties_Food_Amount": "Egenskaper Livsmedel Mängd",
"Open_Data_Slug": "Öppen Data Slug",
"Open_Data_Import": "Öppen Data Import",
"Properties_Food_Unit": "Egenskaper Livsmedel Enhet",
"OrderInformation": "Objekt är sorterade från små till stora siffror.",
"show_step_ingredients_setting": "Visa ingredienser bredvid recept-steg",
"show_step_ingredients_setting_help": "Lägg till tabell med ingredienser bredvid recept-steg. Verkställs vid skapande. Kan skrivas över i redigering av receptvyn.",
"Space_Cosmetic_Settings": "Vissa kosmetiska inställningar kan ändras av hushålls-administratörer och skriver över klientinställningar för det hushållet.",
"show_ingredients_table": "Visa en tabell över ingredienserna bredvid stegets text",
"Enable": "Aktivera",
"CustomTheme": "Anpassat tema",
"CustomThemeHelp": "Skriv över nuvarande tema genom att ladda upp en anpassad CSS-fil.",
"CustomNavLogoHelp": "Ladda upp en bild att använda som meny-logga.",
"CustomImageHelp": "Ladda upp en bild som visas i överblicken.",
"CustomLogoHelp": "Ladda upp kvadratiska bilder i olika storlekar för att ändra logga i webbläsare."
} }

View File

@@ -518,6 +518,7 @@ export class Models {
header_component: { header_component: {
name: "BetaWarning", name: "BetaWarning",
}, },
params: ["automation_type", "page", "pageSize", "options"],
}, },
create: { create: {
params: [["name", "description", "type", "param_1", "param_2", "param_3", "order", "disabled"]], params: [["name", "description", "type", "param_1", "param_2", "param_3", "order", "disabled"]],
@@ -620,7 +621,7 @@ export class Models {
}, },
form_function: "AutomationOrderDefault", form_function: "AutomationOrderDefault",
}, },
}, }
} }
static UNIT_CONVERSION = { static UNIT_CONVERSION = {
@@ -1032,7 +1033,7 @@ export class Models {
static CUSTOM_FILTER = { static CUSTOM_FILTER = {
name: "Custom Filter", name: "Custom Filter",
apiName: "CustomFilter", apiName: "CustomFilter",
paginated: true,
create: { create: {
params: [["name", "search", "shared"]], params: [["name", "search", "shared"]],
form: { form: {
@@ -1055,6 +1056,9 @@ export class Models {
}, },
}, },
}, },
list: {
params: ["page", "pageSize", "options"],
},
} }
static USER_NAME = { static USER_NAME = {
name: "User", name: "User",
@@ -1228,6 +1232,7 @@ export class Models {
static STEP = { static STEP = {
name: "Step", name: "Step",
apiName: "Step", apiName: "Step",
paginated: true,
list: { list: {
params: ["recipe", "query", "page", "pageSize", "options"], params: ["recipe", "query", "page", "pageSize", "options"],
}, },

View File

@@ -1378,10 +1378,10 @@ export interface InlineResponse200 {
previous?: string | null; previous?: string | null;
/** /**
* *
* @type {Array<CookLog>} * @type {Array<Automation>}
* @memberof InlineResponse200 * @memberof InlineResponse200
*/ */
results?: Array<CookLog>; results?: Array<Automation>;
} }
/** /**
* *
@@ -1409,10 +1409,10 @@ export interface InlineResponse2001 {
previous?: string | null; previous?: string | null;
/** /**
* *
* @type {Array<Food>} * @type {Array<CookLog>}
* @memberof InlineResponse2001 * @memberof InlineResponse2001
*/ */
results?: Array<Food>; results?: Array<CookLog>;
} }
/** /**
* *
@@ -1440,10 +1440,10 @@ export interface InlineResponse20010 {
previous?: string | null; previous?: string | null;
/** /**
* *
* @type {Array<Unit>} * @type {Array<SupermarketCategoryRelation>}
* @memberof InlineResponse20010 * @memberof InlineResponse20010
*/ */
results?: Array<Unit>; results?: Array<SupermarketCategoryRelation>;
} }
/** /**
* *
@@ -1471,10 +1471,10 @@ export interface InlineResponse20011 {
previous?: string | null; previous?: string | null;
/** /**
* *
* @type {Array<UserSpace>} * @type {Array<SyncLog>}
* @memberof InlineResponse20011 * @memberof InlineResponse20011
*/ */
results?: Array<UserSpace>; results?: Array<SyncLog>;
} }
/** /**
* *
@@ -1502,9 +1502,71 @@ export interface InlineResponse20012 {
previous?: string | null; previous?: string | null;
/** /**
* *
* @type {Array<ViewLog>} * @type {Array<Unit>}
* @memberof InlineResponse20012 * @memberof InlineResponse20012
*/ */
results?: Array<Unit>;
}
/**
*
* @export
* @interface InlineResponse20013
*/
export interface InlineResponse20013 {
/**
*
* @type {number}
* @memberof InlineResponse20013
*/
count?: number;
/**
*
* @type {string}
* @memberof InlineResponse20013
*/
next?: string | null;
/**
*
* @type {string}
* @memberof InlineResponse20013
*/
previous?: string | null;
/**
*
* @type {Array<UserSpace>}
* @memberof InlineResponse20013
*/
results?: Array<UserSpace>;
}
/**
*
* @export
* @interface InlineResponse20014
*/
export interface InlineResponse20014 {
/**
*
* @type {number}
* @memberof InlineResponse20014
*/
count?: number;
/**
*
* @type {string}
* @memberof InlineResponse20014
*/
next?: string | null;
/**
*
* @type {string}
* @memberof InlineResponse20014
*/
previous?: string | null;
/**
*
* @type {Array<ViewLog>}
* @memberof InlineResponse20014
*/
results?: Array<ViewLog>; results?: Array<ViewLog>;
} }
/** /**
@@ -1533,10 +1595,10 @@ export interface InlineResponse2002 {
previous?: string | null; previous?: string | null;
/** /**
* *
* @type {Array<ImportLog>} * @type {Array<CustomFilter>}
* @memberof InlineResponse2002 * @memberof InlineResponse2002
*/ */
results?: Array<ImportLog>; results?: Array<CustomFilter>;
} }
/** /**
* *
@@ -1564,10 +1626,10 @@ export interface InlineResponse2003 {
previous?: string | null; previous?: string | null;
/** /**
* *
* @type {Array<ExportLog>} * @type {Array<Food>}
* @memberof InlineResponse2003 * @memberof InlineResponse2003
*/ */
results?: Array<ExportLog>; results?: Array<Food>;
} }
/** /**
* *
@@ -1595,10 +1657,10 @@ export interface InlineResponse2004 {
previous?: string | null; previous?: string | null;
/** /**
* *
* @type {Array<Ingredient>} * @type {Array<ImportLog>}
* @memberof InlineResponse2004 * @memberof InlineResponse2004
*/ */
results?: Array<Ingredient>; results?: Array<ImportLog>;
} }
/** /**
* *
@@ -1626,10 +1688,10 @@ export interface InlineResponse2005 {
previous?: string | null; previous?: string | null;
/** /**
* *
* @type {Array<Keyword>} * @type {Array<ExportLog>}
* @memberof InlineResponse2005 * @memberof InlineResponse2005
*/ */
results?: Array<Keyword>; results?: Array<ExportLog>;
} }
/** /**
* *
@@ -1657,10 +1719,10 @@ export interface InlineResponse2006 {
previous?: string | null; previous?: string | null;
/** /**
* *
* @type {Array<RecipeOverview>} * @type {Array<Ingredient>}
* @memberof InlineResponse2006 * @memberof InlineResponse2006
*/ */
results?: Array<RecipeOverview>; results?: Array<Ingredient>;
} }
/** /**
* *
@@ -1688,10 +1750,10 @@ export interface InlineResponse2007 {
previous?: string | null; previous?: string | null;
/** /**
* *
* @type {Array<Step>} * @type {Array<Keyword>}
* @memberof InlineResponse2007 * @memberof InlineResponse2007
*/ */
results?: Array<Step>; results?: Array<Keyword>;
} }
/** /**
* *
@@ -1719,10 +1781,10 @@ export interface InlineResponse2008 {
previous?: string | null; previous?: string | null;
/** /**
* *
* @type {Array<SupermarketCategoryRelation>} * @type {Array<RecipeOverview>}
* @memberof InlineResponse2008 * @memberof InlineResponse2008
*/ */
results?: Array<SupermarketCategoryRelation>; results?: Array<RecipeOverview>;
} }
/** /**
* *
@@ -1750,10 +1812,10 @@ export interface InlineResponse2009 {
previous?: string | null; previous?: string | null;
/** /**
* *
* @type {Array<SyncLog>} * @type {Array<Step>}
* @memberof InlineResponse2009 * @memberof InlineResponse2009
*/ */
results?: Array<SyncLog>; results?: Array<Step>;
} }
/** /**
* *
@@ -8517,11 +8579,14 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
}; };
}, },
/** /**
* * optional parameters - **automation_type**: Return the Automations matching the automation type. Multiple values allowed. *Automation Types:* - FS: Food Alias - UA: Unit Alias - KA: Keyword Alias - DR: Description Replace - IR: Instruction Replace - NU: Never Unit - TW: Transpose Words - FR: Food Replace - UR: Unit Replace - NR: Name Replace
* @param {string} [automationType] Return the Automations matching the automation type. Multiple values allowed.
* @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. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
listAutomations: async (options: any = {}): Promise<RequestArgs> => { listAutomations: async (automationType?: string, page?: number, pageSize?: number, options: any = {}): Promise<RequestArgs> => {
const localVarPath = `/api/automation/`; const localVarPath = `/api/automation/`;
// use dummy base URL string because the URL constructor only accepts absolute URLs. // use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@@ -8534,6 +8599,18 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
const localVarHeaderParameter = {} as any; const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any; const localVarQueryParameter = {} as any;
if (automationType !== undefined) {
localVarQueryParameter['automation_type'] = automationType;
}
if (page !== undefined) {
localVarQueryParameter['page'] = page;
}
if (pageSize !== undefined) {
localVarQueryParameter['page_size'] = pageSize;
}
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query); setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
@@ -8644,10 +8721,12 @@ 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. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
listCustomFilters: async (options: any = {}): Promise<RequestArgs> => { listCustomFilters: async (page?: number, pageSize?: number, options: any = {}): Promise<RequestArgs> => {
const localVarPath = `/api/custom-filter/`; const localVarPath = `/api/custom-filter/`;
// use dummy base URL string because the URL constructor only accepts absolute URLs. // use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@@ -8660,6 +8739,14 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
const localVarHeaderParameter = {} as any; const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any; const localVarQueryParameter = {} as any;
if (page !== undefined) {
localVarQueryParameter['page'] = page;
}
if (pageSize !== undefined) {
localVarQueryParameter['page_size'] = pageSize;
}
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query); setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
@@ -9581,7 +9668,7 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
/** /**
* *
* @param {number} [id] Returns the shopping list entry with a primary key of id. Multiple values allowed. * @param {number} [id] Returns the shopping list entry with a primary key of id. Multiple values allowed.
* @param {string} [checked] Filter shopping list entries on checked. [true, false, both, &lt;b&gt;recent&lt;/b&gt;]&lt;br&gt; - recent includes unchecked items and recently completed items. * @param {string} [checked] Filter shopping list entries on checked. [true, false, both, &lt;b&gt;recent&lt;/b&gt;]&lt;br&gt; - recent includes unchecked items and recently completed items.
* @param {number} [supermarket] Returns the shopping list entries sorted by supermarket category order. * @param {number} [supermarket] Returns the shopping list entries sorted by supermarket category order.
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
@@ -16231,12 +16318,15 @@ export const ApiApiFp = function(configuration?: Configuration) {
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
/** /**
* * optional parameters - **automation_type**: Return the Automations matching the automation type. Multiple values allowed. *Automation Types:* - FS: Food Alias - UA: Unit Alias - KA: Keyword Alias - DR: Description Replace - IR: Instruction Replace - NU: Never Unit - TW: Transpose Words - FR: Food Replace - UR: Unit Replace - NR: Name Replace
* @param {string} [automationType] Return the Automations matching the automation type. Multiple values allowed.
* @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. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async listAutomations(options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<Automation>>> { async listAutomations(automationType?: string, page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse200>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listAutomations(options); const localVarAxiosArgs = await localVarAxiosParamCreator.listAutomations(automationType, page, pageSize, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
/** /**
@@ -16264,19 +16354,10 @@ export const ApiApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async listCookLogs(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse200>> { async listCookLogs(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2001>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listCookLogs(page, pageSize, options); const localVarAxiosArgs = await localVarAxiosParamCreator.listCookLogs(page, pageSize, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async listCustomFilters(options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<CustomFilter>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listCustomFilters(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/** /**
* *
* @param {number} [page] A page number within the paginated result set. * @param {number} [page] A page number within the paginated result set.
@@ -16284,7 +16365,18 @@ export const ApiApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async listExportLogs(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2003>> { async listCustomFilters(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2002>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listCustomFilters(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 listExportLogs(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2005>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listExportLogs(page, pageSize, options); const localVarAxiosArgs = await localVarAxiosParamCreator.listExportLogs(page, pageSize, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
@@ -16307,7 +16399,7 @@ export const ApiApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @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<InlineResponse2003>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listFoods(query, root, tree, page, pageSize, options); const localVarAxiosArgs = await localVarAxiosParamCreator.listFoods(query, root, tree, page, pageSize, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
@@ -16327,7 +16419,7 @@ export const ApiApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async listImportLogs(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2002>> { async listImportLogs(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2004>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listImportLogs(page, pageSize, options); const localVarAxiosArgs = await localVarAxiosParamCreator.listImportLogs(page, pageSize, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
@@ -16338,7 +16430,7 @@ export const ApiApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async listIngredients(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2004>> { async listIngredients(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2006>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listIngredients(page, pageSize, options); const localVarAxiosArgs = await localVarAxiosParamCreator.listIngredients(page, pageSize, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
@@ -16361,7 +16453,7 @@ export const ApiApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async listKeywords(query?: string, root?: number, tree?: number, page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2005>> { async listKeywords(query?: string, root?: number, tree?: number, page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2007>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listKeywords(query, root, tree, page, pageSize, options); const localVarAxiosArgs = await localVarAxiosParamCreator.listKeywords(query, root, tree, page, pageSize, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
@@ -16528,14 +16620,14 @@ export const ApiApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async listRecipes(query?: string, keywords?: number, keywordsOr?: number, keywordsAnd?: number, keywordsOrNot?: number, keywordsAndNot?: number, foods?: number, foodsOr?: number, foodsAnd?: number, foodsOrNot?: number, foodsAndNot?: number, units?: number, rating?: number, books?: string, booksOr?: number, booksAnd?: number, booksOrNot?: number, booksAndNot?: number, internal?: string, random?: string, _new?: string, timescooked?: number, cookedon?: string, createdon?: string, updatedon?: string, viewedon?: string, makenow?: string, page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2006>> { async listRecipes(query?: string, keywords?: number, keywordsOr?: number, keywordsAnd?: number, keywordsOrNot?: number, keywordsAndNot?: number, foods?: number, foodsOr?: number, foodsAnd?: number, foodsOrNot?: number, foodsAndNot?: number, units?: number, rating?: number, books?: string, booksOr?: number, booksAnd?: number, booksOrNot?: number, booksAndNot?: number, internal?: string, random?: string, _new?: string, timescooked?: number, cookedon?: string, createdon?: string, updatedon?: string, viewedon?: string, makenow?: string, page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2008>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listRecipes(query, keywords, keywordsOr, keywordsAnd, keywordsOrNot, keywordsAndNot, foods, foodsOr, foodsAnd, foodsOrNot, foodsAndNot, units, rating, books, booksOr, booksAnd, booksOrNot, booksAndNot, internal, random, _new, timescooked, cookedon, createdon, updatedon, viewedon, makenow, page, pageSize, options); const localVarAxiosArgs = await localVarAxiosParamCreator.listRecipes(query, keywords, keywordsOr, keywordsAnd, keywordsOrNot, keywordsAndNot, foods, foodsOr, foodsAnd, foodsOrNot, foodsAndNot, units, rating, books, booksOr, booksAnd, booksOrNot, booksAndNot, internal, random, _new, timescooked, cookedon, createdon, updatedon, viewedon, makenow, page, pageSize, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
/** /**
* *
* @param {number} [id] Returns the shopping list entry with a primary key of id. Multiple values allowed. * @param {number} [id] Returns the shopping list entry with a primary key of id. Multiple values allowed.
* @param {string} [checked] Filter shopping list entries on checked. [true, false, both, &lt;b&gt;recent&lt;/b&gt;]&lt;br&gt; - recent includes unchecked items and recently completed items. * @param {string} [checked] Filter shopping list entries on checked. [true, false, both, &lt;b&gt;recent&lt;/b&gt;]&lt;br&gt; - recent includes unchecked items and recently completed items.
* @param {number} [supermarket] Returns the shopping list entries sorted by supermarket category order. * @param {number} [supermarket] Returns the shopping list entries sorted by supermarket category order.
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
@@ -16580,7 +16672,7 @@ export const ApiApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async listSteps(recipe?: number, query?: string, page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2007>> { async listSteps(recipe?: number, query?: string, page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2009>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listSteps(recipe, query, page, pageSize, options); const localVarAxiosArgs = await localVarAxiosParamCreator.listSteps(recipe, query, page, pageSize, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
@@ -16600,7 +16692,7 @@ export const ApiApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async listSupermarketCategoryRelations(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2008>> { async listSupermarketCategoryRelations(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse20010>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listSupermarketCategoryRelations(page, pageSize, options); const localVarAxiosArgs = await localVarAxiosParamCreator.listSupermarketCategoryRelations(page, pageSize, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
@@ -16631,7 +16723,7 @@ export const ApiApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async listSyncLogs(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2009>> { async listSyncLogs(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse20011>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listSyncLogs(page, pageSize, options); const localVarAxiosArgs = await localVarAxiosParamCreator.listSyncLogs(page, pageSize, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
@@ -16662,7 +16754,7 @@ export const ApiApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async listUnits(query?: string, page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse20010>> { async listUnits(query?: string, page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse20012>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listUnits(query, page, pageSize, options); const localVarAxiosArgs = await localVarAxiosParamCreator.listUnits(query, page, pageSize, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
@@ -16692,7 +16784,7 @@ export const ApiApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async listUserSpaces(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse20011>> { async listUserSpaces(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse20013>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listUserSpaces(page, pageSize, options); const localVarAxiosArgs = await localVarAxiosParamCreator.listUserSpaces(page, pageSize, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
@@ -16712,7 +16804,7 @@ export const ApiApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async listViewLogs(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse20012>> { async listViewLogs(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse20014>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listViewLogs(page, pageSize, options); const localVarAxiosArgs = await localVarAxiosParamCreator.listViewLogs(page, pageSize, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
@@ -19041,12 +19133,15 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
return localVarFp.listAccessTokens(options).then((request) => request(axios, basePath)); return localVarFp.listAccessTokens(options).then((request) => request(axios, basePath));
}, },
/** /**
* * optional parameters - **automation_type**: Return the Automations matching the automation type. Multiple values allowed. *Automation Types:* - FS: Food Alias - UA: Unit Alias - KA: Keyword Alias - DR: Description Replace - IR: Instruction Replace - NU: Never Unit - TW: Transpose Words - FR: Food Replace - UR: Unit Replace - NR: Name Replace
* @param {string} [automationType] Return the Automations matching the automation type. Multiple values allowed.
* @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. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
listAutomations(options?: any): AxiosPromise<Array<Automation>> { listAutomations(automationType?: string, page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse200> {
return localVarFp.listAutomations(options).then((request) => request(axios, basePath)); return localVarFp.listAutomations(automationType, page, pageSize, options).then((request) => request(axios, basePath));
}, },
/** /**
* *
@@ -19071,17 +19166,9 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
listCookLogs(page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse200> { listCookLogs(page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2001> {
return localVarFp.listCookLogs(page, pageSize, options).then((request) => request(axios, basePath)); return localVarFp.listCookLogs(page, pageSize, options).then((request) => request(axios, basePath));
}, },
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listCustomFilters(options?: any): AxiosPromise<Array<CustomFilter>> {
return localVarFp.listCustomFilters(options).then((request) => request(axios, basePath));
},
/** /**
* *
* @param {number} [page] A page number within the paginated result set. * @param {number} [page] A page number within the paginated result set.
@@ -19089,7 +19176,17 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
listExportLogs(page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2003> { listCustomFilters(page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2002> {
return localVarFp.listCustomFilters(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}
*/
listExportLogs(page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2005> {
return localVarFp.listExportLogs(page, pageSize, options).then((request) => request(axios, basePath)); return localVarFp.listExportLogs(page, pageSize, options).then((request) => request(axios, basePath));
}, },
/** /**
@@ -19110,7 +19207,7 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @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<InlineResponse2003> {
return localVarFp.listFoods(query, root, tree, page, pageSize, options).then((request) => request(axios, basePath)); return localVarFp.listFoods(query, root, tree, page, pageSize, options).then((request) => request(axios, basePath));
}, },
/** /**
@@ -19128,7 +19225,7 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
listImportLogs(page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2002> { listImportLogs(page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2004> {
return localVarFp.listImportLogs(page, pageSize, options).then((request) => request(axios, basePath)); return localVarFp.listImportLogs(page, pageSize, options).then((request) => request(axios, basePath));
}, },
/** /**
@@ -19138,7 +19235,7 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
listIngredients(page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2004> { listIngredients(page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2006> {
return localVarFp.listIngredients(page, pageSize, options).then((request) => request(axios, basePath)); return localVarFp.listIngredients(page, pageSize, options).then((request) => request(axios, basePath));
}, },
/** /**
@@ -19159,7 +19256,7 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
listKeywords(query?: string, root?: number, tree?: number, page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2005> { listKeywords(query?: string, root?: number, tree?: number, page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2007> {
return localVarFp.listKeywords(query, root, tree, page, pageSize, options).then((request) => request(axios, basePath)); return localVarFp.listKeywords(query, root, tree, page, pageSize, options).then((request) => request(axios, basePath));
}, },
/** /**
@@ -19311,13 +19408,13 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
listRecipes(query?: string, keywords?: number, keywordsOr?: number, keywordsAnd?: number, keywordsOrNot?: number, keywordsAndNot?: number, foods?: number, foodsOr?: number, foodsAnd?: number, foodsOrNot?: number, foodsAndNot?: number, units?: number, rating?: number, books?: string, booksOr?: number, booksAnd?: number, booksOrNot?: number, booksAndNot?: number, internal?: string, random?: string, _new?: string, timescooked?: number, cookedon?: string, createdon?: string, updatedon?: string, viewedon?: string, makenow?: string, page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2006> { listRecipes(query?: string, keywords?: number, keywordsOr?: number, keywordsAnd?: number, keywordsOrNot?: number, keywordsAndNot?: number, foods?: number, foodsOr?: number, foodsAnd?: number, foodsOrNot?: number, foodsAndNot?: number, units?: number, rating?: number, books?: string, booksOr?: number, booksAnd?: number, booksOrNot?: number, booksAndNot?: number, internal?: string, random?: string, _new?: string, timescooked?: number, cookedon?: string, createdon?: string, updatedon?: string, viewedon?: string, makenow?: string, page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2008> {
return localVarFp.listRecipes(query, keywords, keywordsOr, keywordsAnd, keywordsOrNot, keywordsAndNot, foods, foodsOr, foodsAnd, foodsOrNot, foodsAndNot, units, rating, books, booksOr, booksAnd, booksOrNot, booksAndNot, internal, random, _new, timescooked, cookedon, createdon, updatedon, viewedon, makenow, page, pageSize, options).then((request) => request(axios, basePath)); return localVarFp.listRecipes(query, keywords, keywordsOr, keywordsAnd, keywordsOrNot, keywordsAndNot, foods, foodsOr, foodsAnd, foodsOrNot, foodsAndNot, units, rating, books, booksOr, booksAnd, booksOrNot, booksAndNot, internal, random, _new, timescooked, cookedon, createdon, updatedon, viewedon, makenow, page, pageSize, options).then((request) => request(axios, basePath));
}, },
/** /**
* *
* @param {number} [id] Returns the shopping list entry with a primary key of id. Multiple values allowed. * @param {number} [id] Returns the shopping list entry with a primary key of id. Multiple values allowed.
* @param {string} [checked] Filter shopping list entries on checked. [true, false, both, &lt;b&gt;recent&lt;/b&gt;]&lt;br&gt; - recent includes unchecked items and recently completed items. * @param {string} [checked] Filter shopping list entries on checked. [true, false, both, &lt;b&gt;recent&lt;/b&gt;]&lt;br&gt; - recent includes unchecked items and recently completed items.
* @param {number} [supermarket] Returns the shopping list entries sorted by supermarket category order. * @param {number} [supermarket] Returns the shopping list entries sorted by supermarket category order.
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
@@ -19358,7 +19455,7 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
listSteps(recipe?: number, query?: string, page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2007> { listSteps(recipe?: number, query?: string, page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2009> {
return localVarFp.listSteps(recipe, query, page, pageSize, options).then((request) => request(axios, basePath)); return localVarFp.listSteps(recipe, query, page, pageSize, options).then((request) => request(axios, basePath));
}, },
/** /**
@@ -19376,7 +19473,7 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
listSupermarketCategoryRelations(page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2008> { listSupermarketCategoryRelations(page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse20010> {
return localVarFp.listSupermarketCategoryRelations(page, pageSize, options).then((request) => request(axios, basePath)); return localVarFp.listSupermarketCategoryRelations(page, pageSize, options).then((request) => request(axios, basePath));
}, },
/** /**
@@ -19404,7 +19501,7 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
listSyncLogs(page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2009> { listSyncLogs(page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse20011> {
return localVarFp.listSyncLogs(page, pageSize, options).then((request) => request(axios, basePath)); return localVarFp.listSyncLogs(page, pageSize, options).then((request) => request(axios, basePath));
}, },
/** /**
@@ -19432,7 +19529,7 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
listUnits(query?: string, page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse20010> { listUnits(query?: string, page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse20012> {
return localVarFp.listUnits(query, page, pageSize, options).then((request) => request(axios, basePath)); return localVarFp.listUnits(query, page, pageSize, options).then((request) => request(axios, basePath));
}, },
/** /**
@@ -19459,7 +19556,7 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
listUserSpaces(page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse20011> { listUserSpaces(page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse20013> {
return localVarFp.listUserSpaces(page, pageSize, options).then((request) => request(axios, basePath)); return localVarFp.listUserSpaces(page, pageSize, options).then((request) => request(axios, basePath));
}, },
/** /**
@@ -19477,7 +19574,7 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
listViewLogs(page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse20012> { listViewLogs(page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse20014> {
return localVarFp.listViewLogs(page, pageSize, options).then((request) => request(axios, basePath)); return localVarFp.listViewLogs(page, pageSize, options).then((request) => request(axios, basePath));
}, },
/** /**
@@ -21837,13 +21934,16 @@ export class ApiApi extends BaseAPI {
} }
/** /**
* * optional parameters - **automation_type**: Return the Automations matching the automation type. Multiple values allowed. *Automation Types:* - FS: Food Alias - UA: Unit Alias - KA: Keyword Alias - DR: Description Replace - IR: Instruction Replace - NU: Never Unit - TW: Transpose Words - FR: Food Replace - UR: Unit Replace - NR: Name Replace
* @param {string} [automationType] Return the Automations matching the automation type. Multiple values allowed.
* @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. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
* @memberof ApiApi * @memberof ApiApi
*/ */
public listAutomations(options?: any) { public listAutomations(automationType?: string, page?: number, pageSize?: number, options?: any) {
return ApiApiFp(this.configuration).listAutomations(options).then((request) => request(this.axios, this.basePath)); return ApiApiFp(this.configuration).listAutomations(automationType, page, pageSize, options).then((request) => request(this.axios, this.basePath));
} }
/** /**
@@ -21880,12 +21980,14 @@ 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. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
* @memberof ApiApi * @memberof ApiApi
*/ */
public listCustomFilters(options?: any) { public listCustomFilters(page?: number, pageSize?: number, options?: any) {
return ApiApiFp(this.configuration).listCustomFilters(options).then((request) => request(this.axios, this.basePath)); return ApiApiFp(this.configuration).listCustomFilters(page, pageSize, options).then((request) => request(this.axios, this.basePath));
} }
/** /**
@@ -22169,7 +22271,7 @@ export class ApiApi extends BaseAPI {
/** /**
* *
* @param {number} [id] Returns the shopping list entry with a primary key of id. Multiple values allowed. * @param {number} [id] Returns the shopping list entry with a primary key of id. Multiple values allowed.
* @param {string} [checked] Filter shopping list entries on checked. [true, false, both, &lt;b&gt;recent&lt;/b&gt;]&lt;br&gt; - recent includes unchecked items and recently completed items. * @param {string} [checked] Filter shopping list entries on checked. [true, false, both, &lt;b&gt;recent&lt;/b&gt;]&lt;br&gt; - recent includes unchecked items and recently completed items.
* @param {number} [supermarket] Returns the shopping list entries sorted by supermarket category order. * @param {number} [supermarket] Returns the shopping list entries sorted by supermarket category order.
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}

View File

@@ -29,10 +29,6 @@ const pages = {
entry: "./src/apps/ExportView/main.ts", entry: "./src/apps/ExportView/main.ts",
chunks: ["chunk-vendors","locales-chunk","api-chunk"], chunks: ["chunk-vendors","locales-chunk","api-chunk"],
}, },
supermarket_view: {
entry: "./src/apps/SupermarketView/main.ts",
chunks: ["chunk-vendors","locales-chunk","api-chunk"],
},
model_list_view: { model_list_view: {
entry: "./src/apps/ModelListView/main.ts", entry: "./src/apps/ModelListView/main.ts",
chunks: ["chunk-vendors","locales-chunk","api-chunk"], chunks: ["chunk-vendors","locales-chunk","api-chunk"],
@@ -65,10 +61,6 @@ const pages = {
entry: "./src/apps/SpaceManageView/main.ts", entry: "./src/apps/SpaceManageView/main.ts",
chunks: ["chunk-vendors","locales-chunk","api-chunk"], chunks: ["chunk-vendors","locales-chunk","api-chunk"],
}, },
profile_view: {
entry: "./src/apps/ProfileView/main.ts",
chunks: ["chunk-vendors","locales-chunk","api-chunk"],
},
settings_view: { settings_view: {
entry: "./src/apps/SettingsView/main.ts", entry: "./src/apps/SettingsView/main.ts",
chunks: ["chunk-vendors","locales-chunk","api-chunk"], chunks: ["chunk-vendors","locales-chunk","api-chunk"],