mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-01 12:18:45 -05:00
Merge branch 'develop' into feature/vue3
# Conflicts: # recipes/settings.py # vue/vue.config.js
This commit is contained in:
@@ -13,7 +13,7 @@ from cookbook.managers import DICTIONARY
|
||||
from .models import (BookmarkletImport, Comment, CookLog, Food, ImportLog, Ingredient, InviteLink,
|
||||
Keyword, MealPlan, MealType, NutritionInformation, Property, PropertyType,
|
||||
Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchPreference, ShareLink,
|
||||
ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
|
||||
ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
|
||||
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog,
|
||||
TelegramBot, Unit, UnitConversion, UserFile, UserPreference, UserSpace,
|
||||
ViewLog, ConnectorConfig)
|
||||
@@ -369,13 +369,6 @@ class ShoppingListEntryAdmin(admin.ModelAdmin):
|
||||
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):
|
||||
list_display = ('recipe', 'created_by', 'uuid', 'created_at',)
|
||||
|
||||
|
||||
@@ -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)'), }
|
||||
|
||||
|
||||
|
||||
class ConnectorConfigForm(forms.ModelForm):
|
||||
enabled = forms.BooleanField(
|
||||
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):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -239,7 +222,6 @@ class SyncForm(forms.ModelForm):
|
||||
labels = {'storage': _('Storage'), 'path': _('Path'), 'active': _('Active')}
|
||||
|
||||
|
||||
# TODO deprecate
|
||||
class BatchEditForm(forms.Form):
|
||||
search = forms.CharField(label=_('Search String'))
|
||||
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':
|
||||
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
|
||||
# }
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import cookbook.helper.dal
|
||||
from cookbook.helper.AllAuthCustomAdapter import AllAuthCustomAdapter
|
||||
|
||||
__all__ = [
|
||||
'dal',
|
||||
]
|
||||
|
||||
@@ -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
|
||||
@@ -75,7 +75,7 @@ def is_object_owner(user, obj):
|
||||
if not user.is_authenticated:
|
||||
return False
|
||||
try:
|
||||
return obj.get_owner() == user
|
||||
return obj.get_owner() == 'orphan' or obj.get_owner() == user
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db.models import F, OuterRef, Q, Subquery, Value
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from cookbook.models import (Ingredient, MealPlan, Recipe, ShoppingListEntry, ShoppingListRecipe,
|
||||
@@ -76,10 +75,8 @@ class RecipeShoppingEditor():
|
||||
|
||||
@staticmethod
|
||||
def get_shopping_list_recipe(id, user, space):
|
||||
return ShoppingListRecipe.objects.filter(id=id).filter(Q(shoppinglist__space=space) | Q(entries__space=space)).filter(
|
||||
Q(shoppinglist__created_by=user)
|
||||
| Q(shoppinglist__shared=user)
|
||||
| Q(entries__created_by=user)
|
||||
return ShoppingListRecipe.objects.filter(id=id).filter(entries__space=space).filter(
|
||||
Q(entries__created_by=user)
|
||||
| Q(entries__created_by__in=list(user.get_shopping_share()))
|
||||
).prefetch_related('entries').first()
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-04-11 15:09+0200\n"
|
||||
"PO-Revision-Date: 2022-04-17 00:31+0000\n"
|
||||
"Last-Translator: Oskar Stenberg <01ste02@gmail.com>\n"
|
||||
"PO-Revision-Date: 2024-02-27 12:19+0000\n"
|
||||
"Last-Translator: Lukas Åteg <lukas@ategsolutions.se>\n"
|
||||
"Language-Team: Swedish <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/sv/>\n"
|
||||
"Language: sv\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\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\templates\forms\edit_internal_recipe.html:219
|
||||
@@ -1812,7 +1812,7 @@ msgstr "Google ld+json info"
|
||||
|
||||
#: .\cookbook\templates\url_import.html:268
|
||||
msgid "GitHub Issues"
|
||||
msgstr "GitHub Issues"
|
||||
msgstr "GitHub Problem"
|
||||
|
||||
#: .\cookbook\templates\url_import.html:270
|
||||
msgid "Recipe Markup Specification"
|
||||
@@ -1852,7 +1852,7 @@ msgstr "Kunde inte tolka korrekt..."
|
||||
msgid "Batch edit done. %(count)d recipe was updated."
|
||||
msgid_plural "Batch edit done. %(count)d Recipes where updated."
|
||||
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
|
||||
msgid "Monitor"
|
||||
|
||||
18
cookbook/migrations/0214_delete_shopping_model.py
Normal file
18
cookbook/migrations/0214_delete_shopping_model.py
Normal 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",
|
||||
),
|
||||
]
|
||||
@@ -369,9 +369,8 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
|
||||
Storage.objects.filter(space=self).delete()
|
||||
ConnectorConfig.objects.filter(space=self).delete()
|
||||
|
||||
ShoppingListEntry.objects.filter(shoppinglist__space=self).delete()
|
||||
ShoppingListRecipe.objects.filter(shoppinglist__space=self).delete()
|
||||
ShoppingList.objects.filter(space=self).delete()
|
||||
ShoppingListEntry.objects.filter(space=self).delete()
|
||||
ShoppingListRecipe.objects.filter(recipe__space=self).delete()
|
||||
|
||||
SupermarketCategoryRelation.objects.filter(supermarket__space=self).delete()
|
||||
SupermarketCategory.objects.filter(space=self).delete()
|
||||
@@ -1195,7 +1194,10 @@ class ShoppingListRecipe(ExportModelOperationsMixin('shopping_list_recipe'), mod
|
||||
|
||||
def get_owner(self):
|
||||
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:
|
||||
return None
|
||||
|
||||
@@ -1218,53 +1220,19 @@ class ShoppingListEntry(ExportModelOperationsMixin('shopping_list_entry'), model
|
||||
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||
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):
|
||||
return f'Shopping list entry {self.id}'
|
||||
|
||||
def get_shared(self):
|
||||
try:
|
||||
return self.shoppinglist_set.first().shared.all()
|
||||
except AttributeError:
|
||||
return self.created_by.userpreference.shopping_share.all()
|
||||
return self.created_by.userpreference.shopping_share.all()
|
||||
|
||||
def get_owner(self):
|
||||
try:
|
||||
return self.created_by or self.shoppinglist_set.first().created_by
|
||||
return self.created_by
|
||||
except AttributeError:
|
||||
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):
|
||||
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
|
||||
uuid = models.UUIDField(default=uuid.uuid4)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import traceback
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
@@ -31,7 +30,7 @@ from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, Cu
|
||||
ExportLog, Food, FoodInheritField, ImportLog, Ingredient, InviteLink,
|
||||
Keyword, MealPlan, MealType, NutritionInformation, Property,
|
||||
PropertyType, Recipe, RecipeBook, RecipeBookEntry, RecipeImport,
|
||||
ShareLink, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space,
|
||||
ShareLink, ShoppingListEntry, ShoppingListRecipe, Space,
|
||||
Step, Storage, Supermarket, SupermarketCategory,
|
||||
SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion,
|
||||
UserFile, UserPreference, UserSpace, ViewLog, ConnectorConfig)
|
||||
@@ -82,14 +81,12 @@ class ExtendedRecipeMixin(serializers.ModelSerializer):
|
||||
class OpenDataModelMixin(serializers.ModelSerializer):
|
||||
|
||||
def create(self, 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() == '':
|
||||
if 'open_data_slug' in validated_data and validated_data['open_data_slug'] is not None and validated_data['open_data_slug'].strip() == '':
|
||||
validated_data['open_data_slug'] = None
|
||||
return super().create(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[
|
||||
'open_data_slug'].strip() == '':
|
||||
if 'open_data_slug' in validated_data and validated_data['open_data_slug'] is not None and validated_data['open_data_slug'].strip() == '':
|
||||
validated_data['open_data_slug'] = None
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
@@ -336,8 +333,7 @@ class UserSpaceSerializer(WritableNestedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = UserSpace
|
||||
fields = (
|
||||
'id', 'user', 'space', 'groups', 'active', 'internal_note', 'invite_link', 'created_at', 'updated_at',)
|
||||
fields = ('id', 'user', 'space', 'groups', 'active', 'internal_note', 'invite_link', 'created_at', 'updated_at',)
|
||||
read_only_fields = ('id', 'invite_link', 'created_at', 'updated_at', 'space')
|
||||
|
||||
|
||||
@@ -877,8 +873,7 @@ class UnitConversionSerializer(WritableNestedModelSerializer, OpenDataModelMixin
|
||||
|
||||
class Meta:
|
||||
model = UnitConversion
|
||||
fields = (
|
||||
'id', 'name', 'base_amount', 'base_unit', 'converted_amount', 'converted_unit', 'food', 'open_data_slug')
|
||||
fields = ('id', 'name', 'base_amount', 'base_unit', 'converted_amount', 'converted_unit', 'food', 'open_data_slug')
|
||||
|
||||
|
||||
class NutritionInformationSerializer(serializers.ModelSerializer):
|
||||
@@ -1198,37 +1193,6 @@ class ShoppingListEntryCheckedSerializer(serializers.ModelSerializer):
|
||||
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 Meta:
|
||||
model = ShareLink
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
class="fas fa-fw fa-calendar"></i> {% trans 'Meal-Plan' %}</a>
|
||||
</li>
|
||||
<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>
|
||||
</li>
|
||||
<li class="nav-item {% if request.resolver_match.url_name in 'view_books' %}active{% endif %}">
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -214,7 +214,7 @@ def test_completed(sle, u1_s1):
|
||||
|
||||
def test_recent(sle, 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()
|
||||
|
||||
today_start = timezone.now().replace(hour=0, minute=0, second=0)
|
||||
|
||||
@@ -3,9 +3,8 @@ 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, ShoppingListRecipe
|
||||
from cookbook.models import ShoppingListEntry, ShoppingListRecipe
|
||||
|
||||
LIST_URL = 'api:shoppinglistrecipe-list'
|
||||
DETAIL_URL = 'api:shoppinglistrecipe-detail'
|
||||
@@ -14,81 +13,31 @@ DETAIL_URL = 'api:shoppinglistrecipe-detail'
|
||||
@pytest.fixture()
|
||||
def obj_1(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)
|
||||
for ing in r.recipe.steps.first().ingredients.all():
|
||||
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
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
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],
|
||||
])
|
||||
@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()
|
||||
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],
|
||||
])
|
||||
@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}
|
||||
),
|
||||
{'servings': 2},
|
||||
content_type='application/json'
|
||||
)
|
||||
r = c.patch(reverse(DETAIL_URL, args={obj_1.id}), {'servings': 2}, content_type='application/json')
|
||||
assert r.status_code == arg[1]
|
||||
if r.status_code == 200:
|
||||
response = json.loads(r.content)
|
||||
assert response['servings'] == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", [
|
||||
['a_u', 403],
|
||||
['g1_s1', 201],
|
||||
['u1_s1', 201],
|
||||
['a1_s1', 201],
|
||||
])
|
||||
@pytest.mark.parametrize("arg", [['a_u', 403], ['g1_s1', 201], ['u1_s1', 201], ['a1_s1', 201], ])
|
||||
def test_add(arg, request, obj_1, recipe_1_s1):
|
||||
c = request.getfixturevalue(arg[0])
|
||||
r = c.post(
|
||||
reverse(LIST_URL),
|
||||
{'recipe': recipe_1_s1.pk, 'servings': 1},
|
||||
content_type='application/json'
|
||||
)
|
||||
r = c.post(reverse(LIST_URL), {'recipe': recipe_1_s1.pk, 'servings': 1}, content_type='application/json')
|
||||
response = json.loads(r.content)
|
||||
print(r.content)
|
||||
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):
|
||||
r = u1_s2.delete(
|
||||
reverse(
|
||||
DETAIL_URL,
|
||||
args={obj_1.id}
|
||||
)
|
||||
)
|
||||
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}
|
||||
)
|
||||
)
|
||||
r = u1_s1.delete(reverse(DETAIL_URL, args={obj_1.id}))
|
||||
|
||||
assert r.status_code == 204
|
||||
|
||||
@@ -6,7 +6,7 @@ from django.contrib import auth
|
||||
from django.urls import reverse
|
||||
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'
|
||||
DETAIL_URL = 'api:unit-detail'
|
||||
@@ -50,8 +50,6 @@ def ing_3_s2(obj_3, space_2, u2_s2):
|
||||
@pytest.fixture()
|
||||
def sle_1_s1(obj_1, u1_s1, space_1):
|
||||
e = ShoppingListEntry.objects.create(unit=obj_1, food=random_food(space_1, u1_s1), 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
|
||||
|
||||
|
||||
@@ -63,8 +61,6 @@ def sle_2_s1(obj_2, u1_s1, space_1):
|
||||
@pytest.fixture()
|
||||
def sle_3_s2(obj_3, u2_s2, space_2):
|
||||
e = ShoppingListEntry.objects.create(unit=obj_3, food=random_food(space_2, u2_s2), 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
|
||||
|
||||
|
||||
|
||||
@@ -5,20 +5,22 @@ from django.views.generic import TemplateView
|
||||
from rest_framework import permissions, routers
|
||||
from rest_framework.schemas import get_schema_view
|
||||
|
||||
from cookbook.helper import dal
|
||||
from cookbook.version_info import TANDOOR_VERSION
|
||||
from recipes.settings import DEBUG, PLUGINS
|
||||
|
||||
|
||||
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,
|
||||
UserFile, UserSpace, get_model_name, ConnectorConfig)
|
||||
|
||||
from .views import api, data, delete, edit, import_export, lists, new, telegram, views
|
||||
from .views.api import CustomAuthToken, ImportOpenData
|
||||
|
||||
|
||||
# extend DRF default router class to allow including additional routers
|
||||
class DefaultRouter(routers.DefaultRouter):
|
||||
|
||||
def extend(self, r):
|
||||
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'food-property-type', api.PropertyTypeViewSet) # TODO rename + regenerate
|
||||
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-recipe', api.ShoppingListRecipeViewSet)
|
||||
router.register(r'space', api.SpaceViewSet)
|
||||
@@ -80,7 +81,6 @@ urlpatterns = [
|
||||
path('space-overview', views.space_overview, name='view_space_overview'),
|
||||
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('profile/<int:user_id>', views.view_profile, name='view_profile'),
|
||||
path('no-perm', views.no_perm, name='view_no_perm'),
|
||||
path('invite/<slug:token>', views.invite_link, name='view_invite'),
|
||||
path('system/', views.system, name='view_system'),
|
||||
@@ -89,34 +89,27 @@ urlpatterns = [
|
||||
path('plan/', views.meal_plan, name='view_plan'),
|
||||
path('shopping/', lists.shopping_list, name='view_shopping'),
|
||||
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('supermarket/', views.supermarket, name='view_supermarket'),
|
||||
path('ingredient-editor/', views.ingredient_editor, name='view_ingredient_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('api/import/', api.import_files, name='view_import'),
|
||||
path('import-response/<int:pk>/', import_export.import_response, name='view_import_response'),
|
||||
path('export/', import_export.export_recipe, name='view_export'),
|
||||
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('view/recipe/<int:pk>', 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/share-link/<int:pk>/', new.share_link, name='new_share_link'),
|
||||
|
||||
path('edit/recipe/<int:pk>/', edit.switch_recipe, name='edit_recipe'),
|
||||
|
||||
# for internal use only
|
||||
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/convert/<int:pk>/', edit.convert_recipe, name='edit_convert_recipe'),
|
||||
|
||||
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'),
|
||||
|
||||
# TODO move to generic "new" view
|
||||
@@ -125,7 +118,6 @@ urlpatterns = [
|
||||
path('data/batch/import', data.batch_import, name='data_batch_import'),
|
||||
path('data/sync/wait', data.sync_wait, name='data_sync_wait'),
|
||||
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_recipe_file/<int:recipe_id>/', api.get_recipe_file, name='api_get_recipe_file'),
|
||||
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/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('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/remove/<int:pk>', telegram.remove_bot, name='telegram_remove'),
|
||||
path('telegram/hook/<slug:token>/', telegram.hook, name='telegram_hook'),
|
||||
|
||||
path('docs/markdown/', views.markdown_info, name='docs_markdown'),
|
||||
path('docs/search/', views.search_info, name='docs_search'),
|
||||
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-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
||||
path('api-token-auth/', CustomAuthToken.as_view()),
|
||||
path('api-import-open-data/', ImportOpenData.as_view(), name='api_import_open_data'),
|
||||
|
||||
path('offline/', views.offline, name='view_offline'),
|
||||
|
||||
path('service-worker.js', (TemplateView.as_view(template_name="sw.js", content_type='application/javascript', )),
|
||||
name='service_worker'),
|
||||
path('service-worker.js', (TemplateView.as_view(template_name="sw.js", content_type='application/javascript',
|
||||
)), name='service_worker'),
|
||||
path('manifest.json', views.web_manifest, name='web_manifest'),
|
||||
]
|
||||
|
||||
|
||||
generic_models = (
|
||||
Recipe, RecipeImport, Storage, ConnectorConfig, RecipeBook, SyncLog, Sync,
|
||||
Comment, RecipeBookEntry, ShoppingList, InviteLink, UserSpace, Space
|
||||
Comment, RecipeBookEntry, InviteLink, UserSpace, Space
|
||||
)
|
||||
|
||||
|
||||
for m in generic_models:
|
||||
py_name = get_model_name(m)
|
||||
url_name = py_name.replace('_', '-')
|
||||
|
||||
if c := locate(f'cookbook.views.new.{m.__name__}Create'):
|
||||
urlpatterns.append(
|
||||
path(
|
||||
f'new/{url_name}/', c.as_view(), name=f'new_{py_name}'
|
||||
)
|
||||
)
|
||||
urlpatterns.append(path(f'new/{url_name}/', c.as_view(), name=f'new_{py_name}'))
|
||||
|
||||
if c := locate(f'cookbook.views.edit.{m.__name__}Update'):
|
||||
urlpatterns.append(
|
||||
path(
|
||||
f'edit/{url_name}/<int:pk>/',
|
||||
c.as_view(),
|
||||
name=f'edit_{py_name}'
|
||||
)
|
||||
)
|
||||
urlpatterns.append(path(f'edit/{url_name}/<int:pk>/', c.as_view(), name=f'edit_{py_name}'))
|
||||
|
||||
if c := getattr(lists, py_name, None):
|
||||
urlpatterns.append(
|
||||
path(
|
||||
f'list/{url_name}/', c, name=f'list_{py_name}'
|
||||
)
|
||||
)
|
||||
urlpatterns.append(path(f'list/{url_name}/', c, name=f'list_{py_name}'))
|
||||
|
||||
if c := locate(f'cookbook.views.delete.{m.__name__}Delete'):
|
||||
urlpatterns.append(
|
||||
path(
|
||||
f'delete/{url_name}/<int:pk>/',
|
||||
c.as_view(),
|
||||
name=f'delete_{py_name}'
|
||||
)
|
||||
)
|
||||
urlpatterns.append(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]
|
||||
for m in vue_models:
|
||||
@@ -214,11 +176,7 @@ for m in vue_models:
|
||||
url_name = py_name.replace('_', '-')
|
||||
|
||||
if c := getattr(lists, py_name, None):
|
||||
urlpatterns.append(
|
||||
path(
|
||||
f'list/{url_name}/', c, name=f'list_{py_name}'
|
||||
)
|
||||
)
|
||||
urlpatterns.append(path(f'list/{url_name}/', c, name=f'list_{py_name}'))
|
||||
|
||||
if DEBUG:
|
||||
urlpatterns.append(path('test/', views.test, name='view_test'))
|
||||
|
||||
@@ -14,6 +14,7 @@ from zipfile import ZipFile
|
||||
|
||||
import requests
|
||||
import validators
|
||||
from PIL import UnidentifiedImageError
|
||||
from annoying.decorators import ajax_request
|
||||
from annoying.functions import get_object_or_None
|
||||
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.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.timezone import make_aware
|
||||
from django.utils.translation import gettext as _
|
||||
from django_scopes import scopes_disabled
|
||||
from icalendar import Calendar, Event
|
||||
from oauth2_provider.models import AccessToken
|
||||
from PIL import UnidentifiedImageError
|
||||
from recipe_scrapers import scrape_me
|
||||
from recipe_scrapers._exceptions import NoSchemaFoundInWildMode
|
||||
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.ingredient_parser import IngredientParser
|
||||
from cookbook.helper.open_data_importer import OpenDataImporter
|
||||
from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsOwner, CustomIsOwnerReadOnly,
|
||||
CustomIsShared, CustomIsSpaceOwner, CustomIsUser,
|
||||
CustomRecipePermission, CustomTokenHasReadWriteScope,
|
||||
CustomTokenHasScope, CustomUserPermission,
|
||||
IsReadOnlyDRF, above_space_limit, group_required,
|
||||
has_group_permission, is_space_owner,
|
||||
switch_user_active_space)
|
||||
from cookbook.helper.permission_helper import (
|
||||
CustomIsAdmin, CustomIsOwner, CustomIsOwnerReadOnly, CustomIsShared, CustomIsSpaceOwner, CustomIsUser, CustomRecipePermission, CustomTokenHasReadWriteScope,
|
||||
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_url_import import (clean_dict, get_from_youtube_scraper,
|
||||
get_images_from_soup)
|
||||
from cookbook.helper.recipe_url_import import clean_dict, get_from_youtube_scraper, get_images_from_soup
|
||||
from cookbook.helper.scrapers.scrapers import text_scraper
|
||||
from cookbook.helper.shopping_helper import RecipeShoppingEditor, shopping_helper
|
||||
from cookbook.models import (Automation, BookmarkletImport, CookLog, CustomFilter, ExportLog, Food,
|
||||
FoodInheritField, FoodProperty, ImportLog, Ingredient, InviteLink,
|
||||
Keyword, MealPlan, MealType, Property, PropertyType, Recipe,
|
||||
RecipeBook, RecipeBookEntry, ShareLink, ShoppingList,
|
||||
ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
|
||||
RecipeBook, RecipeBookEntry, ShareLink, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
|
||||
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync,
|
||||
SyncLog, Unit, UnitConversion, UserFile, UserPreference, UserSpace,
|
||||
ViewLog, ConnectorConfig)
|
||||
@@ -96,9 +90,8 @@ from cookbook.serializer import (AccessTokenSerializer, AutomationSerializer,
|
||||
RecipeExportSerializer, RecipeFromSourceSerializer,
|
||||
RecipeImageSerializer, RecipeOverviewSerializer, RecipeSerializer,
|
||||
RecipeShoppingUpdateSerializer, RecipeSimpleSerializer,
|
||||
ShoppingListAutoSyncSerializer, ShoppingListEntrySerializer,
|
||||
ShoppingListRecipeSerializer, ShoppingListSerializer,
|
||||
SpaceSerializer, StepSerializer, StorageSerializer,
|
||||
ShoppingListEntrySerializer,
|
||||
ShoppingListRecipeSerializer, SpaceSerializer, StepSerializer, StorageSerializer,
|
||||
SupermarketCategoryRelationSerializer,
|
||||
SupermarketCategorySerializer, SupermarketSerializer,
|
||||
SyncLogSerializer, SyncSerializer, UnitConversionSerializer,
|
||||
@@ -107,10 +100,11 @@ from cookbook.serializer import (AccessTokenSerializer, AutomationSerializer,
|
||||
ShoppingListEntryBulkSerializer, ConnectorConfigConfigSerializer, RecipeFlatSerializer)
|
||||
from cookbook.views.import_export import get_integration
|
||||
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):
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = self.queryset
|
||||
query = self.request.query_params.get('query', None)
|
||||
@@ -161,12 +155,13 @@ class ExtendedRecipeMixin():
|
||||
queryset = queryset.annotate(recipe_count=Coalesce(Subquery(recipe_count), 0))
|
||||
|
||||
# add a recipe image annotation to the query
|
||||
image_subquery = Recipe.objects.filter(**{recipe_filter: OuterRef('id')}, space=space).exclude(
|
||||
image__isnull=True).exclude(image__exact='').order_by("?").values('image')[:1]
|
||||
image_subquery = Recipe.objects.filter(**{
|
||||
recipe_filter: OuterRef('id')
|
||||
}, space=space).exclude(image__isnull=True).exclude(image__exact='').order_by("?").values('image')[:1]
|
||||
if tree:
|
||||
image_children_subquery = Recipe.objects.filter(
|
||||
**{f"{recipe_filter}__path__startswith": OuterRef('path')},
|
||||
space=space).exclude(image__isnull=True).exclude(image__exact='').order_by("?").values('image')[:1]
|
||||
image_children_subquery = Recipe.objects.filter(**{
|
||||
f"{recipe_filter}__path__startswith": OuterRef('path')
|
||||
}, space=space).exclude(image__isnull=True).exclude(image__exact='').order_by("?").values('image')[:1]
|
||||
else:
|
||||
image_children_subquery = None
|
||||
if images:
|
||||
@@ -183,17 +178,17 @@ class FuzzyFilterMixin(ViewSetMixin, ExtendedRecipeMixin):
|
||||
self.queryset = self.queryset.filter(space=self.request.space).order_by(Lower('name').asc())
|
||||
query = self.request.query_params.get('query', None)
|
||||
if self.request.user.is_authenticated:
|
||||
fuzzy = self.request.user.searchpreference.lookup or any([self.model.__name__.lower() in x for x in
|
||||
self.request.user.searchpreference.trigram.values_list(
|
||||
'field', flat=True)])
|
||||
fuzzy = self.request.user.searchpreference.lookup or any(
|
||||
[self.model.__name__.lower() in x for x in self.request.user.searchpreference.trigram.values_list('field', flat=True)])
|
||||
else:
|
||||
fuzzy = True
|
||||
|
||||
if query is not None and query not in ["''", '']:
|
||||
if fuzzy and (settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql'):
|
||||
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.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name__unaccent', query))
|
||||
[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))
|
||||
else:
|
||||
self.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name', query))
|
||||
self.queryset = self.queryset.order_by('-trigram')
|
||||
@@ -205,10 +200,9 @@ class FuzzyFilterMixin(ViewSetMixin, ExtendedRecipeMixin):
|
||||
filter |= Q(name__unaccent__icontains=query)
|
||||
|
||||
self.queryset = (
|
||||
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
|
||||
.filter(filter).order_by('-starts', Lower('name').asc())
|
||||
)
|
||||
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
|
||||
.filter(filter).order_by('-starts',
|
||||
Lower('name').asc()))
|
||||
|
||||
updated_at = self.request.query_params.get('updated_at', None)
|
||||
if updated_at is not None:
|
||||
@@ -229,6 +223,7 @@ class FuzzyFilterMixin(ViewSetMixin, ExtendedRecipeMixin):
|
||||
|
||||
|
||||
class MergeMixin(ViewSetMixin):
|
||||
|
||||
@decorators.action(detail=True, url_path='merge/(?P<target>[^/.]+)', methods=['PUT'], )
|
||||
@decorators.renderer_classes((TemplateHTMLRenderer, JSONRenderer))
|
||||
def merge(self, request, pk, target):
|
||||
@@ -296,8 +291,7 @@ class MergeMixin(ViewSetMixin):
|
||||
return Response(content, status=status.HTTP_200_OK)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
content = {'error': True,
|
||||
'msg': _(f'An error occurred attempting to merge {source.name} with {target.name}')}
|
||||
content = {'error': True, 'msg': _(f'An error occurred attempting to merge {source.name} with {target.name}')}
|
||||
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)
|
||||
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,
|
||||
tree=True)
|
||||
return self.annotate_recipe(queryset=self.queryset, request=self.request, serializer=self.serializer_class, tree=True)
|
||||
|
||||
@decorators.action(detail=True, url_path='move/(?P<parent>[^/.]+)', methods=['PUT'], )
|
||||
@decorators.renderer_classes((TemplateHTMLRenderer, JSONRenderer))
|
||||
@@ -561,8 +554,7 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
|
||||
|
||||
def get_queryset(self):
|
||||
shared_users = []
|
||||
if c := caches['default'].get(
|
||||
f'shopping_shared_users_{self.request.space.id}_{self.request.user.id}', None):
|
||||
if c := caches['default'].get(f'shopping_shared_users_{self.request.space.id}_{self.request.user.id}', None):
|
||||
shared_users = c
|
||||
else:
|
||||
try:
|
||||
@@ -573,8 +565,7 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
|
||||
pass
|
||||
|
||||
self.queryset = super().get_queryset()
|
||||
shopping_status = ShoppingListEntry.objects.filter(space=self.request.space, food=OuterRef('id'),
|
||||
checked=False).values('id')
|
||||
shopping_status = ShoppingListEntry.objects.filter(space=self.request.space, food=OuterRef('id'), checked=False).values('id')
|
||||
# onhand_status = self.queryset.annotate(onhand_status=Exists(onhand_users_set__in=[shared_users]))
|
||||
return self.queryset \
|
||||
.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.append(request.user)
|
||||
if request.data.get('_delete', False) == 'true':
|
||||
ShoppingListEntry.objects.filter(food=obj, checked=False, space=request.space,
|
||||
created_by__in=shared_users).delete()
|
||||
ShoppingListEntry.objects.filter(food=obj, checked=False, space=request.space, created_by__in=shared_users).delete()
|
||||
content = {'msg': _(f'{obj.name} was removed from the shopping list.')}
|
||||
return Response(content, status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -604,8 +594,7 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
|
||||
unit = request.data.get('unit', None)
|
||||
content = {'msg': _(f'{obj.name} was added to the shopping list.')}
|
||||
|
||||
ShoppingListEntry.objects.create(food=obj, amount=amount, unit=unit, space=request.space,
|
||||
created_by=request.user)
|
||||
ShoppingListEntry.objects.create(food=obj, amount=amount, unit=unit, space=request.space, created_by=request.user)
|
||||
return Response(content, status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@decorators.action(detail=True, methods=['POST'], )
|
||||
@@ -616,19 +605,32 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
|
||||
"""
|
||||
food = self.get_object()
|
||||
if not food.fdc_id:
|
||||
return JsonResponse({'msg': 'Food has no FDC ID associated.'}, status=400,
|
||||
json_dumps_params={'indent': 4})
|
||||
return JsonResponse({'msg': 'Food has no FDC ID associated.'}, status=400, 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}')
|
||||
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,
|
||||
json_dumps_params={'indent': 4})
|
||||
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,
|
||||
json_dumps_params={'indent': 4})
|
||||
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})
|
||||
|
||||
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()
|
||||
|
||||
try:
|
||||
@@ -694,8 +696,7 @@ class RecipeBookViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
||||
|
||||
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(
|
||||
space=self.request.space).distinct().order_by(ordering)
|
||||
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)
|
||||
return super().get_queryset()
|
||||
|
||||
|
||||
@@ -713,9 +714,7 @@ class RecipeBookEntryViewSet(viewsets.ModelViewSet, viewsets.GenericViewSet):
|
||||
permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope]
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = self.queryset.filter(
|
||||
Q(book__created_by=self.request.user) | Q(book__shared=self.request.user)).filter(
|
||||
book__space=self.request.space).distinct()
|
||||
queryset = self.queryset.filter(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)
|
||||
if recipe_id is not None:
|
||||
@@ -748,10 +747,7 @@ class MealPlanViewSet(viewsets.ModelViewSet):
|
||||
schema = QueryParamAutoSchema()
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = self.queryset.filter(
|
||||
Q(created_by=self.request.user)
|
||||
| Q(shared=self.request.user)
|
||||
).filter(space=self.request.space).distinct().all()
|
||||
queryset = self.queryset.filter(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)
|
||||
if from_date is not None:
|
||||
@@ -769,6 +765,7 @@ class MealPlanViewSet(viewsets.ModelViewSet):
|
||||
|
||||
|
||||
class AutoPlanViewSet(viewsets.ViewSet):
|
||||
|
||||
def create(self, request):
|
||||
serializer = AutoMealPlanSerializer(data=request.data)
|
||||
|
||||
@@ -798,10 +795,16 @@ class AutoPlanViewSet(viewsets.ViewSet):
|
||||
for i in range(0, days):
|
||||
day = start_date + datetime.timedelta(i)
|
||||
recipe = recipes[i % len(recipes)]
|
||||
args = {'recipe_id': recipe['id'], 'servings': servings,
|
||||
'created_by': request.user,
|
||||
'meal_type_id': serializer.validated_data['meal_type_id'],
|
||||
'note': '', 'from_date': day, 'to_date': day, 'space': request.space}
|
||||
args = {
|
||||
'recipe_id': recipe['id'],
|
||||
'servings': servings,
|
||||
'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)
|
||||
meal_plans.append(m)
|
||||
@@ -816,12 +819,7 @@ class AutoPlanViewSet(viewsets.ViewSet):
|
||||
SLR.create(mealplan=m, servings=servings)
|
||||
|
||||
else:
|
||||
post_save.send(
|
||||
sender=m.__class__,
|
||||
instance=m,
|
||||
created=True,
|
||||
update_fields=None,
|
||||
)
|
||||
post_save.send(sender=m.__class__, instance=m, created=True, update_fields=None, )
|
||||
|
||||
return Response(serializer.data)
|
||||
|
||||
@@ -838,8 +836,7 @@ class MealTypeViewSet(viewsets.ModelViewSet):
|
||||
permission_classes = [CustomIsOwner & CustomTokenHasReadWriteScope]
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = self.queryset.order_by('order', 'id').filter(created_by=self.request.user).filter(
|
||||
space=self.request.space).all()
|
||||
queryset = self.queryset.order_by('order', 'id').filter(created_by=self.request.user).filter(space=self.request.space).all()
|
||||
return queryset
|
||||
|
||||
|
||||
@@ -899,12 +896,7 @@ class RecipePagination(PageNumberPagination):
|
||||
return super().paginate_queryset(queryset, request, view)
|
||||
|
||||
def get_paginated_response(self, data):
|
||||
return Response(OrderedDict([
|
||||
('count', self.page.paginator.count),
|
||||
('next', self.get_next_link()),
|
||||
('previous', self.get_previous_link()),
|
||||
('results', data),
|
||||
]))
|
||||
return Response(OrderedDict([('count', self.page.paginator.count), ('next', self.get_next_link()), ('previous', self.get_previous_link()), ('results', data), ]))
|
||||
|
||||
|
||||
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 not share: # filter for space only if not shared
|
||||
self.queryset = self.queryset.filter(space=self.request.space).prefetch_related(
|
||||
'keywords',
|
||||
'shared',
|
||||
'properties',
|
||||
'properties__property_type',
|
||||
'steps',
|
||||
'steps__ingredients',
|
||||
'steps__ingredients__step_set',
|
||||
'steps__ingredients__step_set__recipe_set',
|
||||
'steps__ingredients__food',
|
||||
'steps__ingredients__food__properties',
|
||||
'steps__ingredients__food__properties__property_type',
|
||||
'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')
|
||||
self.queryset = self.queryset.filter(
|
||||
space=self.request.space).prefetch_related('keywords', 'shared', 'properties', 'properties__property_type', 'steps', 'steps__ingredients',
|
||||
'steps__ingredients__step_set', 'steps__ingredients__step_set__recipe_set', 'steps__ingredients__food',
|
||||
'steps__ingredients__food__properties', 'steps__ingredients__food__properties__property_type',
|
||||
'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()
|
||||
|
||||
self.queryset = self.queryset.filter(space=self.request.space).filter(
|
||||
Q(private=False) | (Q(private=True) & (Q(created_by=self.request.user) | Q(shared=self.request.user)))
|
||||
)
|
||||
self.queryset = self.queryset.filter(
|
||||
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
|
||||
in list(self.request.GET)}
|
||||
params = {x: self.request.GET.get(x) if len({**self.request.GET}[x]) == 1 else self.request.GET.getlist(x) for x in list(self.request.GET)}
|
||||
search = RecipeSearch(self.request, **params)
|
||||
self.queryset = search.get_queryset(self.queryset).prefetch_related('keywords', 'cooklog_set')
|
||||
return self.queryset
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
if self.request.GET.get('debug', False):
|
||||
return JsonResponse({
|
||||
'new': str(self.get_queryset().query),
|
||||
})
|
||||
return JsonResponse({'new': str(self.get_queryset().query), })
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
def get_serializer_class(self):
|
||||
@@ -1000,12 +975,7 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
||||
return RecipeOverviewSerializer
|
||||
return self.serializer_class
|
||||
|
||||
@decorators.action(
|
||||
detail=True,
|
||||
methods=['PUT'],
|
||||
serializer_class=RecipeImageSerializer,
|
||||
parser_classes=[MultiPartParser],
|
||||
)
|
||||
@decorators.action(detail=True, methods=['PUT'], serializer_class=RecipeImageSerializer, parser_classes=[MultiPartParser], )
|
||||
def image(self, request, pk):
|
||||
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
|
||||
# DRF only allows one action in a decorator action without overriding get_operation_id_base()
|
||||
@decorators.action(
|
||||
detail=True,
|
||||
methods=['PUT'],
|
||||
serializer_class=RecipeShoppingUpdateSerializer,
|
||||
)
|
||||
@decorators.action(detail=True, methods=['PUT'], serializer_class=RecipeShoppingUpdateSerializer, )
|
||||
def shopping(self, request, pk):
|
||||
if self.request.space.demo:
|
||||
raise PermissionDenied(detail='Not available in demo', code=None)
|
||||
@@ -1087,11 +1053,7 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
||||
|
||||
return Response(content, status=http_status)
|
||||
|
||||
@decorators.action(
|
||||
detail=True,
|
||||
methods=['GET'],
|
||||
serializer_class=RecipeSimpleSerializer
|
||||
)
|
||||
@decorators.action(detail=True, methods=['GET'], serializer_class=RecipeSimpleSerializer)
|
||||
def related(self, request, pk):
|
||||
obj = self.get_object()
|
||||
if obj.get_space() != request.space:
|
||||
@@ -1100,8 +1062,7 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
||||
levels = int(request.query_params.get('levels', 1))
|
||||
except (ValueError, TypeError):
|
||||
levels = 1
|
||||
qs = obj.get_related_recipes(
|
||||
levels=levels) # TODO: make levels a user setting, included in request data?, keep solely in the backend?
|
||||
qs = obj.get_related_recipes(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)
|
||||
|
||||
@decorators.action(
|
||||
@@ -1157,14 +1118,12 @@ class ShoppingListRecipeViewSet(viewsets.ModelViewSet):
|
||||
permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope]
|
||||
|
||||
def get_queryset(self):
|
||||
self.queryset = self.queryset.filter(
|
||||
Q(shoppinglist__space=self.request.space) | Q(entries__space=self.request.space))
|
||||
self.queryset = self.queryset.filter(Q(entries__space=self.request.space) | Q(recipe__space=self.request.space))
|
||||
return self.queryset.filter(
|
||||
Q(shoppinglist__created_by=self.request.user)
|
||||
| Q(shoppinglist__shared=self.request.user)
|
||||
Q(entries__isnull=True)
|
||||
| Q(entries__created_by=self.request.user)
|
||||
| Q(entries__created_by__in=list(self.request.user.get_shopping_share()))
|
||||
).distinct().all()
|
||||
).distinct().all()
|
||||
|
||||
|
||||
class ShoppingListEntryViewSet(viewsets.ModelViewSet):
|
||||
@@ -1173,7 +1132,10 @@ class ShoppingListEntryViewSet(viewsets.ModelViewSet):
|
||||
permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope]
|
||||
query_params = [
|
||||
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'),
|
||||
]
|
||||
@@ -1184,24 +1146,11 @@ class ShoppingListEntryViewSet(viewsets.ModelViewSet):
|
||||
|
||||
self.queryset = self.queryset.filter(
|
||||
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',
|
||||
'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()
|
||||
| Q(created_by__in=list(self.request.user.get_shopping_share()))).prefetch_related('created_by', 'food', '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', []):
|
||||
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:
|
||||
last_autosync = datetime.datetime.fromtimestamp(int(last_autosync) / 1000, datetime.timezone.utc)
|
||||
self.queryset = self.queryset.filter(updated_at__gte=last_autosync)
|
||||
except:
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
|
||||
# TODO once old shopping list is removed this needs updated to sharing users in preferences
|
||||
@@ -1227,53 +1176,20 @@ class ShoppingListEntryViewSet(viewsets.ModelViewSet):
|
||||
else:
|
||||
return self.queryset[:1000]
|
||||
|
||||
@decorators.action(
|
||||
detail=False,
|
||||
methods=['POST'],
|
||||
serializer_class=ShoppingListEntryBulkSerializer,
|
||||
permission_classes=[CustomIsUser]
|
||||
)
|
||||
@decorators.action(detail=False, methods=['POST'], serializer_class=ShoppingListEntryBulkSerializer, permission_classes=[CustomIsUser])
|
||||
def bulk(self, request):
|
||||
serializer = self.serializer_class(data=request.data)
|
||||
if serializer.is_valid():
|
||||
ShoppingListEntry.objects.filter(
|
||||
Q(created_by=self.request.user)
|
||||
| Q(shoppinglist__shared=self.request.user)
|
||||
| Q(created_by__in=list(self.request.user.get_shopping_share()))
|
||||
).filter(space=request.space, id__in=serializer.validated_data['ids']).update(
|
||||
checked=serializer.validated_data['checked'],
|
||||
updated_at=timezone.now(),
|
||||
)
|
||||
ShoppingListEntry.objects.filter(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']
|
||||
).update(checked=serializer.validated_data['checked'],
|
||||
updated_at=timezone.now(),
|
||||
)
|
||||
return Response(serializer.data)
|
||||
else:
|
||||
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):
|
||||
queryset = ViewLog.objects
|
||||
serializer_class = ViewLogSerializer
|
||||
@@ -1348,11 +1264,52 @@ class UserFileViewSet(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
|
||||
serializer_class = AutomationSerializer
|
||||
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):
|
||||
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()
|
||||
return super().get_queryset()
|
||||
|
||||
@@ -1379,10 +1336,10 @@ class CustomFilterViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
||||
queryset = CustomFilter.objects
|
||||
serializer_class = CustomFilterSerializer
|
||||
permission_classes = [CustomIsOwner & CustomTokenHasReadWriteScope]
|
||||
pagination_class = DefaultPagination
|
||||
|
||||
def get_queryset(self):
|
||||
self.queryset = self.queryset.filter(Q(created_by=self.request.user) | Q(shared=self.request.user)).filter(
|
||||
space=self.request.space).distinct()
|
||||
self.queryset = self.queryset.filter(Q(created_by=self.request.user) | Q(shared=self.request.user)).filter(space=self.request.space).distinct()
|
||||
return super().get_queryset()
|
||||
|
||||
|
||||
@@ -1397,6 +1354,7 @@ class AccessTokenViewSet(viewsets.ModelViewSet):
|
||||
|
||||
# -------------- DRF custom views --------------------
|
||||
|
||||
|
||||
class AuthTokenThrottle(AnonRateThrottle):
|
||||
rate = '10/day'
|
||||
|
||||
@@ -1409,15 +1367,14 @@ class CustomAuthToken(ObtainAuthToken):
|
||||
throttle_classes = [AuthTokenThrottle]
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
serializer = self.serializer_class(data=request.data,
|
||||
context={'request': request})
|
||||
serializer = self.serializer_class(data=request.data, context={'request': request})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
user = serializer.validated_data['user']
|
||||
if token := AccessToken.objects.filter(user=user, expires__gt=timezone.now(), scope__contains='read').filter(
|
||||
scope__contains='write').first():
|
||||
if token := AccessToken.objects.filter(user=user, expires__gt=timezone.now(), scope__contains='read').filter(scope__contains='write').first():
|
||||
access_token = token
|
||||
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)),
|
||||
scope='read write app')
|
||||
return Response({
|
||||
@@ -1447,8 +1404,7 @@ class RecipeUrlImportView(APIView):
|
||||
serializer = RecipeFromSourceSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
|
||||
if (b_pk := serializer.validated_data.get('bookmarklet', None)) and (
|
||||
bookmarklet := BookmarkletImport.objects.filter(pk=b_pk).first()):
|
||||
if (b_pk := serializer.validated_data.get('bookmarklet', None)) and (bookmarklet := BookmarkletImport.objects.filter(pk=b_pk).first()):
|
||||
serializer.validated_data['url'] = bookmarklet.url
|
||||
serializer.validated_data['data'] = bookmarklet.html
|
||||
bookmarklet.delete()
|
||||
@@ -1456,61 +1412,40 @@ class RecipeUrlImportView(APIView):
|
||||
url = serializer.validated_data.get('url', None)
|
||||
data = unquote(serializer.validated_data.get('data', None))
|
||||
if not url and not data:
|
||||
return Response({
|
||||
'error': True,
|
||||
'msg': _('Nothing to do.')
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response({'error': True, 'msg': _('Nothing to do.')}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
elif url and not data:
|
||||
if re.match('^(https?://)?(www\\.youtube\\.com|youtu\\.be)/.+$', url):
|
||||
if validators.url(url, public=True):
|
||||
return Response({
|
||||
'recipe_json': get_from_youtube_scraper(url, request),
|
||||
'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):
|
||||
return Response({'recipe_json': get_from_youtube_scraper(url, request), '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(
|
||||
url.replace('/view/recipe/', '/api/recipe/').replace(re.split('/view/recipe/[0-9]+', url)[1],
|
||||
'') + '?share=' +
|
||||
re.split('/view/recipe/[0-9]+', url)[1].replace('/', '')).json()
|
||||
url.replace('/view/recipe/', '/api/recipe/').replace(re.split('/view/recipe/[0-9]+', url)[1], '') + '?share='
|
||||
+ re.split('/view/recipe/[0-9]+', url)[1].replace('/', '')).json()
|
||||
recipe_json = clean_dict(recipe_json, 'id')
|
||||
serialized_recipe = RecipeExportSerializer(data=recipe_json, context={'request': request})
|
||||
if serialized_recipe.is_valid():
|
||||
recipe = serialized_recipe.save()
|
||||
if validators.url(recipe_json['image'], public=True):
|
||||
recipe.image = File(handle_image(request,
|
||||
File(io.BytesIO(requests.get(recipe_json['image']).content),
|
||||
name='image'),
|
||||
File(io.BytesIO(requests.get(recipe_json['image']).content), name='image'),
|
||||
filetype=pathlib.Path(recipe_json['image']).suffix),
|
||||
name=f'{uuid.uuid4()}_{recipe.pk}{pathlib.Path(recipe_json["image"]).suffix}')
|
||||
recipe.save()
|
||||
return Response({
|
||||
'link': request.build_absolute_uri(reverse('view_recipe', args={recipe.pk}))
|
||||
}, status=status.HTTP_201_CREATED)
|
||||
return Response({'link': request.build_absolute_uri(reverse('view_recipe', args={recipe.pk}))}, status=status.HTTP_201_CREATED)
|
||||
else:
|
||||
try:
|
||||
if validators.url(url, public=True):
|
||||
scrape = scrape_me(url_path=url, wild_mode=True)
|
||||
|
||||
else:
|
||||
return Response({
|
||||
'error': True,
|
||||
'msg': _('Invalid Url')
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response({'error': True, 'msg': _('Invalid Url')}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except NoSchemaFoundInWildMode:
|
||||
pass
|
||||
except requests.exceptions.ConnectionError:
|
||||
return Response({
|
||||
'error': True,
|
||||
'msg': _('Connection Refused.')
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response({'error': True, 'msg': _('Connection Refused.')}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except requests.exceptions.MissingSchema:
|
||||
return Response({
|
||||
'error': True,
|
||||
'msg': _('Bad URL Schema.')
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response({'error': True, 'msg': _('Bad URL Schema.')}, status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
try:
|
||||
data_json = json.loads(data)
|
||||
@@ -1529,13 +1464,11 @@ class RecipeUrlImportView(APIView):
|
||||
return Response({
|
||||
'recipe_json': helper.get_from_scraper(scrape, request),
|
||||
'recipe_images': list(dict.fromkeys(get_images_from_soup(scrape.soup, url))),
|
||||
}, status=status.HTTP_200_OK)
|
||||
},
|
||||
status=status.HTTP_200_OK)
|
||||
|
||||
else:
|
||||
return Response({
|
||||
'error': True,
|
||||
'msg': _('No usable data could be found.')
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response({'error': True, 'msg': _('No usable data could be found.')}, status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
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)
|
||||
except NotImplementedError:
|
||||
return Response({'error': True, 'msg': _('Importing is not implemented for this provider')},
|
||||
status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response({'error': True, 'msg': _('Importing is not implemented for this provider')}, status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
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')
|
||||
def sync_all(request):
|
||||
if request.space.demo or settings.HOSTED:
|
||||
messages.add_message(request, messages.ERROR,
|
||||
_('This feature is not yet available in the hosted version of tandoor!'))
|
||||
messages.add_message(request, messages.ERROR, _('This feature is not yet available in the hosted version of tandoor!'))
|
||||
return redirect('index')
|
||||
|
||||
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
|
||||
|
||||
if not error:
|
||||
messages.add_message(
|
||||
request, messages.SUCCESS, _('Sync successful!')
|
||||
)
|
||||
messages.add_message(request, messages.SUCCESS, _('Sync successful!'))
|
||||
return redirect('list_recipe_import')
|
||||
else:
|
||||
messages.add_message(
|
||||
request, messages.ERROR, _('Error synchronizing with Storage')
|
||||
)
|
||||
messages.add_message(request, messages.ERROR, _('Error synchronizing with Storage'))
|
||||
return redirect('list_recipe_import')
|
||||
|
||||
|
||||
@@ -1749,11 +1676,10 @@ def sync_all(request):
|
||||
# @schema(AutoSchema()) #TODO add proper schema
|
||||
@permission_classes([CustomIsUser & CustomTokenHasReadWriteScope])
|
||||
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)
|
||||
link = ShareLink.objects.create(recipe=recipe, created_by=request.user, space=request.space)
|
||||
return JsonResponse({'pk': pk, 'share': link.uuid,
|
||||
'link': request.build_absolute_uri(reverse('view_recipe', args=[pk, link.uuid]))})
|
||||
return JsonResponse({'pk': pk, 'share': link.uuid, 'link': request.build_absolute_uri(reverse('view_recipe', args=[pk, link.uuid]))})
|
||||
else:
|
||||
return JsonResponse({'error': 'sharing_disabled'}, status=403)
|
||||
|
||||
@@ -1779,9 +1705,8 @@ def log_cooking(request, recipe_id):
|
||||
|
||||
@group_required('user')
|
||||
def get_plan_ical(request, from_date, to_date):
|
||||
queryset = MealPlan.objects.filter(
|
||||
Q(created_by=request.user) | Q(shared=request.user)
|
||||
).filter(space=request.user.userspace_set.filter(active=1).first().space).distinct().all()
|
||||
queryset = MealPlan.objects.filter(Q(created_by=request.user)
|
||||
| Q(shared=request.user)).filter(space=request.user.userspace_set.filter(active=1).first().space).distinct().all()
|
||||
|
||||
if from_date is not None:
|
||||
queryset = queryset.filter(from_date__gte=from_date)
|
||||
@@ -1822,12 +1747,4 @@ def ingredient_from_string(request):
|
||||
ingredient_parser = IngredientParser(request, False)
|
||||
amount, unit, food, note = ingredient_parser.parse(text)
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
'amount': amount,
|
||||
'unit': unit,
|
||||
'food': food,
|
||||
'note': note
|
||||
},
|
||||
status=200
|
||||
)
|
||||
return JsonResponse({'amount': amount, 'unit': unit, 'food': food, 'note': note}, status=200)
|
||||
|
||||
@@ -8,8 +8,12 @@ from django.utils.translation import gettext as _
|
||||
from django.views.generic import DeleteView
|
||||
|
||||
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,
|
||||
RecipeImport, Space, Storage, Sync, UserSpace, ConnectorConfig)
|
||||
|
||||
from cookbook.provider.dropbox import Dropbox
|
||||
from cookbook.provider.local import Local
|
||||
from cookbook.provider.nextcloud import Nextcloud
|
||||
@@ -114,11 +118,9 @@ class StorageDelete(GroupRequiredMixin, DeleteView):
|
||||
try:
|
||||
return self.delete(request, *args, **kwargs)
|
||||
except ProtectedError:
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.WARNING,
|
||||
_('Could not delete this storage backend as it is used in at least one monitor.') # noqa: E501
|
||||
)
|
||||
messages.add_message(request, messages.WARNING,
|
||||
_('Could not delete this storage backend as it is used in at least one monitor.') # noqa: E501
|
||||
)
|
||||
return HttpResponseRedirect(reverse('list_storage'))
|
||||
|
||||
|
||||
@@ -145,40 +147,6 @@ class CommentDelete(OwnerRequiredMixin, DeleteView):
|
||||
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):
|
||||
template_name = "generic/delete_template.html"
|
||||
model = InviteLink
|
||||
|
||||
@@ -15,7 +15,7 @@ class RecipeCreate(GroupRequiredMixin, CreateView):
|
||||
groups_required = ['user']
|
||||
template_name = "generic/new_template.html"
|
||||
model = Recipe
|
||||
fields = ('name',)
|
||||
fields = ('name', )
|
||||
|
||||
def form_valid(self, form):
|
||||
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!'))
|
||||
else:
|
||||
new_recipe = get_object_or_404(RecipeImport, pk=import_id, space=request.space)
|
||||
form = ImportRecipeForm(
|
||||
initial={
|
||||
'file_path': new_recipe.file_path,
|
||||
'name': new_recipe.name,
|
||||
'file_uid': new_recipe.file_uid
|
||||
}, space=request.space
|
||||
)
|
||||
form = ImportRecipeForm(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})
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from io import StringIO
|
||||
from uuid import UUID
|
||||
@@ -17,7 +15,6 @@ from django.core.management import call_command
|
||||
from django.db import models
|
||||
from django.http import HttpResponseRedirect, JsonResponse
|
||||
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.utils import timezone
|
||||
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.templatetags.theming_tags import get_theming_values
|
||||
from cookbook.version_info import VERSION_INFO
|
||||
from recipes.settings import BASE_DIR, PLUGINS
|
||||
from recipes.settings import PLUGINS
|
||||
|
||||
|
||||
def index(request):
|
||||
@@ -160,8 +157,8 @@ def recipe_view(request, pk, share=None):
|
||||
servings = recipe.servings
|
||||
if request.method == "GET" and 'servings' in request.GET:
|
||||
servings = request.GET.get("servings")
|
||||
return render(request, 'recipe_view.html',
|
||||
{'recipe': recipe, 'comments': comments, 'comment_form': comment_form, 'share': share, 'servings': servings})
|
||||
return render(request, 'recipe_view.html', {'recipe': recipe, 'comments': comments, 'comment_form': comment_form, 'share': share, 'servings': servings})
|
||||
|
||||
|
||||
|
||||
@group_required('user')
|
||||
@@ -174,16 +171,6 @@ def meal_plan(request):
|
||||
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')
|
||||
def user_settings(request):
|
||||
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:
|
||||
messages.add_message(
|
||||
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'))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user