mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2025-12-24 02:39:20 -05:00
remove ShoppingList model and tests
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)
|
||||
@@ -361,13 +361,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',)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
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",
|
||||
),
|
||||
]
|
||||
@@ -368,9 +368,8 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
|
||||
Sync.objects.filter(space=self).delete()
|
||||
Storage.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()
|
||||
@@ -1169,7 +1168,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
|
||||
|
||||
@@ -1192,53 +1194,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)
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -845,8 +841,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):
|
||||
@@ -1150,7 +1145,7 @@ class ShoppingListEntrySerializer(WritableNestedModelSerializer):
|
||||
'recipe_mealplan',
|
||||
'created_by', 'created_at', 'updated_at', 'completed_at', 'delay_until'
|
||||
)
|
||||
read_only_fields = ('id', 'created_by', 'created_at','updated_at',)
|
||||
read_only_fields = ('id', 'created_by', 'created_at', 'updated_at',)
|
||||
|
||||
|
||||
class ShoppingListEntryBulkSerializer(serializers.Serializer):
|
||||
@@ -1165,37 +1160,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,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
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ 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)
|
||||
from .views import api, data, delete, edit, import_export, lists, new, telegram, views
|
||||
@@ -45,7 +45,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)
|
||||
@@ -87,7 +86,7 @@ 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'),
|
||||
@@ -167,7 +166,7 @@ urlpatterns = [
|
||||
|
||||
generic_models = (
|
||||
Recipe, RecipeImport, Storage, RecipeBook, SyncLog, Sync,
|
||||
Comment, RecipeBookEntry, ShoppingList, InviteLink, UserSpace, Space
|
||||
Comment, RecipeBookEntry, InviteLink, UserSpace, Space
|
||||
)
|
||||
|
||||
for m in generic_models:
|
||||
|
||||
@@ -30,7 +30,6 @@ 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
|
||||
@@ -58,59 +57,40 @@ 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,
|
||||
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync,
|
||||
SyncLog, Unit, UnitConversion, UserFile, UserPreference, UserSpace,
|
||||
ViewLog)
|
||||
from cookbook.models import (Automation, BookmarkletImport, CookLog, CustomFilter, ExportLog, Food, FoodInheritField, FoodProperty, ImportLog, Ingredient, InviteLink, Keyword,
|
||||
MealPlan, MealType, Property, PropertyType, Recipe, RecipeBook, RecipeBookEntry, ShareLink, ShoppingListEntry, ShoppingListRecipe, Space, Step,
|
||||
Storage, Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion, UserFile, UserPreference, UserSpace,
|
||||
ViewLog,
|
||||
)
|
||||
from cookbook.provider.dropbox import Dropbox
|
||||
from cookbook.provider.local import Local
|
||||
from cookbook.provider.nextcloud import Nextcloud
|
||||
from cookbook.schemas import FilterSchema, QueryParam, QueryParamAutoSchema, TreeSchema
|
||||
from cookbook.serializer import (AccessTokenSerializer, AutomationSerializer,
|
||||
AutoMealPlanSerializer, BookmarkletImportListSerializer,
|
||||
BookmarkletImportSerializer, CookLogSerializer,
|
||||
CustomFilterSerializer, ExportLogSerializer,
|
||||
FoodInheritFieldSerializer, FoodSerializer,
|
||||
FoodShoppingUpdateSerializer, FoodSimpleSerializer,
|
||||
GroupSerializer, ImportLogSerializer, IngredientSerializer,
|
||||
IngredientSimpleSerializer, InviteLinkSerializer,
|
||||
KeywordSerializer, MealPlanSerializer, MealTypeSerializer,
|
||||
PropertySerializer, PropertyTypeSerializer,
|
||||
RecipeBookEntrySerializer, RecipeBookSerializer,
|
||||
RecipeExportSerializer, RecipeFromSourceSerializer,
|
||||
RecipeImageSerializer, RecipeOverviewSerializer, RecipeSerializer,
|
||||
RecipeShoppingUpdateSerializer, RecipeSimpleSerializer,
|
||||
ShoppingListAutoSyncSerializer, ShoppingListEntrySerializer,
|
||||
ShoppingListRecipeSerializer, ShoppingListSerializer,
|
||||
SpaceSerializer, StepSerializer, StorageSerializer,
|
||||
SupermarketCategoryRelationSerializer,
|
||||
SupermarketCategorySerializer, SupermarketSerializer,
|
||||
SyncLogSerializer, SyncSerializer, UnitConversionSerializer,
|
||||
UnitSerializer, UserFileSerializer, UserPreferenceSerializer,
|
||||
UserSerializer, UserSpaceSerializer, ViewLogSerializer,
|
||||
ShoppingListEntryBulkSerializer)
|
||||
from cookbook.serializer import (AccessTokenSerializer, AutomationSerializer, AutoMealPlanSerializer, BookmarkletImportListSerializer, BookmarkletImportSerializer,
|
||||
CookLogSerializer, CustomFilterSerializer, ExportLogSerializer, FoodInheritFieldSerializer, FoodSerializer, FoodShoppingUpdateSerializer,
|
||||
FoodSimpleSerializer, GroupSerializer, ImportLogSerializer, IngredientSerializer, IngredientSimpleSerializer, InviteLinkSerializer,
|
||||
KeywordSerializer, MealPlanSerializer, MealTypeSerializer, PropertySerializer, PropertyTypeSerializer, RecipeBookEntrySerializer,
|
||||
RecipeBookSerializer, RecipeExportSerializer, RecipeFromSourceSerializer, RecipeImageSerializer, RecipeOverviewSerializer, RecipeSerializer,
|
||||
RecipeShoppingUpdateSerializer, RecipeSimpleSerializer, ShoppingListEntryBulkSerializer, ShoppingListEntrySerializer,
|
||||
ShoppingListRecipeSerializer, SpaceSerializer, StepSerializer, StorageSerializer, SupermarketCategoryRelationSerializer,
|
||||
SupermarketCategorySerializer, SupermarketSerializer, SyncLogSerializer, SyncSerializer, UnitConversionSerializer, UnitSerializer,
|
||||
UserFileSerializer, UserPreferenceSerializer, UserSerializer, UserSpaceSerializer, ViewLogSerializer,
|
||||
)
|
||||
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 +141,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 +164,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 +186,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 +209,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 +277,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 +310,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))
|
||||
@@ -552,23 +531,18 @@ 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:
|
||||
shared_users = [x.id for x in list(self.request.user.get_shopping_share())] + [
|
||||
self.request.user.id]
|
||||
caches['default'].set(
|
||||
f'shopping_shared_users_{self.request.space.id}_{self.request.user.id}',
|
||||
shared_users, timeout=5 * 60)
|
||||
shared_users = [x.id for x in list(self.request.user.get_shopping_share())] + [self.request.user.id]
|
||||
caches['default'].set(f'shopping_shared_users_{self.request.space.id}_{self.request.user.id}', shared_users, timeout=5 * 60)
|
||||
# TODO ugly hack that improves API performance significantly, should be done properly
|
||||
except AttributeError: # Anonymous users (using share links) don't have shared users
|
||||
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)) \
|
||||
@@ -589,8 +563,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)
|
||||
|
||||
@@ -598,8 +571,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'], )
|
||||
@@ -610,19 +582,31 @@ 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:
|
||||
@@ -641,27 +625,27 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
|
||||
for fn in data['foodNutrients']:
|
||||
if fn['nutrient']['id'] == pt.fdc_id:
|
||||
property_found = True
|
||||
food_property_list.append(Property(
|
||||
property_type_id=pt.id,
|
||||
property_amount=max(0, round(fn['amount'], 2)), # sometimes FDC might return negative values which make no sense, set to 0
|
||||
import_food_id=food.id,
|
||||
space=self.request.space,
|
||||
))
|
||||
food_property_list.append(
|
||||
Property(property_type_id=pt.id,
|
||||
property_amount=max(0, round(fn['amount'], 2)), # sometimes FDC might return negative values which make no sense, set to 0
|
||||
import_food_id=food.id,
|
||||
space=self.request.space,
|
||||
))
|
||||
if not property_found:
|
||||
food_property_list.append(Property(
|
||||
property_type_id=pt.id,
|
||||
property_amount=0, # if field not in FDC data the food does not have that property
|
||||
import_food_id=food.id,
|
||||
space=self.request.space,
|
||||
))
|
||||
food_property_list.append(
|
||||
Property(property_type_id=pt.id,
|
||||
property_amount=0, # if field not in FDC data the food does not have that property
|
||||
import_food_id=food.id,
|
||||
space=self.request.space,
|
||||
))
|
||||
|
||||
Property.objects.bulk_create(food_property_list, ignore_conflicts=True, unique_fields=('space', 'import_food_id', 'property_type',))
|
||||
Property.objects.bulk_create(food_property_list, ignore_conflicts=True, unique_fields=('space', 'import_food_id', 'property_type', ))
|
||||
|
||||
property_food_relation_list = []
|
||||
for p in Property.objects.filter(space=self.request.space, import_food_id=food.id).values_list('import_food_id', 'id', ):
|
||||
property_food_relation_list.append(Food.properties.through(food_id=p[0], property_id=p[1]))
|
||||
|
||||
FoodProperty.objects.bulk_create(property_food_relation_list, ignore_conflicts=True, unique_fields=('food_id', 'property_id',))
|
||||
FoodProperty.objects.bulk_create(property_food_relation_list, ignore_conflicts=True, unique_fields=('food_id', 'property_id', ))
|
||||
Property.objects.filter(space=self.request.space, import_food_id=food.id).update(import_food_id=None)
|
||||
|
||||
return self.retrieve(request, pk)
|
||||
@@ -691,8 +675,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()
|
||||
|
||||
|
||||
@@ -710,9 +693,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:
|
||||
@@ -745,10 +726,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:
|
||||
@@ -766,6 +744,7 @@ class MealPlanViewSet(viewsets.ModelViewSet):
|
||||
|
||||
|
||||
class AutoPlanViewSet(viewsets.ViewSet):
|
||||
|
||||
def create(self, request):
|
||||
serializer = AutoMealPlanSerializer(data=request.data)
|
||||
|
||||
@@ -795,10 +774,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)
|
||||
@@ -813,12 +798,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)
|
||||
|
||||
@@ -835,8 +815,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
|
||||
|
||||
|
||||
@@ -896,12 +875,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):
|
||||
@@ -930,15 +904,15 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
||||
QueryParam(name='books_and', description=_('Book IDs, repeat for multiple. Return recipes with all of the books.'), qtype='int'),
|
||||
QueryParam(name='books_or_not', description=_('Book IDs, repeat for multiple. Exclude recipes with any of the books.'), qtype='int'),
|
||||
QueryParam(name='books_and_not', description=_('Book IDs, repeat for multiple. Exclude recipes with all of the books.'), qtype='int'),
|
||||
QueryParam(name='internal', description=_('If only internal recipes should be returned. [''true''/''<b>false</b>'']')),
|
||||
QueryParam(name='random', description=_('Returns the results in randomized order. [''true''/''<b>false</b>'']')),
|
||||
QueryParam(name='new', description=_('Returns new results first in search results. [''true''/''<b>false</b>'']')),
|
||||
QueryParam(name='internal', description=_('If only internal recipes should be returned. [true/<b>false</b>]')),
|
||||
QueryParam(name='random', description=_('Returns the results in randomized order. [true/<b>false</b>]')),
|
||||
QueryParam(name='new', description=_('Returns new results first in search results. [true/<b>false</b>]')),
|
||||
QueryParam(name='timescooked', description=_('Filter recipes cooked X times or more. Negative values returns cooked less than X times'), qtype='int'),
|
||||
QueryParam(name='cookedon', description=_('Filter recipes last cooked on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
||||
QueryParam(name='createdon', description=_('Filter recipes created on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
||||
QueryParam(name='updatedon', description=_('Filter recipes updated on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
||||
QueryParam(name='viewedon', description=_('Filter recipes lasts viewed on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
||||
QueryParam(name='makenow', description=_('Filter recipes that can be made with OnHand food. [''true''/''<b>false</b>'']')),
|
||||
QueryParam(name='cookedon', description=_('Filter recipes last cooked on or after YYYY-MM-DD. Prepending - filters on or before date.')),
|
||||
QueryParam(name='createdon', description=_('Filter recipes created on or after YYYY-MM-DD. Prepending - filters on or before date.')),
|
||||
QueryParam(name='updatedon', description=_('Filter recipes updated on or after YYYY-MM-DD. Prepending - filters on or before date.')),
|
||||
QueryParam(name='viewedon', description=_('Filter recipes lasts viewed on or after YYYY-MM-DD. Prepending - filters on or before date.')),
|
||||
QueryParam(name='makenow', description=_('Filter recipes that can be made with OnHand food. [true/<b>false</b>]')),
|
||||
]
|
||||
schema = QueryParamAutoSchema()
|
||||
|
||||
@@ -947,49 +921,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):
|
||||
@@ -997,12 +954,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()
|
||||
|
||||
@@ -1050,11 +1002,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)
|
||||
@@ -1084,11 +1032,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:
|
||||
@@ -1097,8 +1041,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)
|
||||
|
||||
|
||||
@@ -1106,9 +1049,7 @@ class UnitConversionViewSet(viewsets.ModelViewSet):
|
||||
queryset = UnitConversion.objects
|
||||
serializer_class = UnitConversionSerializer
|
||||
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
|
||||
query_params = [
|
||||
QueryParam(name='food_id', description='ID of food to filter for', qtype='int'),
|
||||
]
|
||||
query_params = [QueryParam(name='food_id', description='ID of food to filter for', qtype='int'), ]
|
||||
schema = QueryParamAutoSchema()
|
||||
|
||||
def get_queryset(self):
|
||||
@@ -1143,14 +1084,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):
|
||||
@@ -1159,8 +1098,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='int'),
|
||||
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 includeks unchecked items and recently completed items.'
|
||||
)),
|
||||
QueryParam(name='supermarket', description=_('Returns the shopping list entries sorted by supermarket category order.'), qtype='int'),
|
||||
]
|
||||
schema = QueryParamAutoSchema()
|
||||
@@ -1170,24 +1111,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])
|
||||
@@ -1204,7 +1132,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
|
||||
@@ -1213,53 +1141,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
|
||||
@@ -1362,8 +1257,7 @@ class CustomFilterViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
||||
permission_classes = [CustomIsOwner & CustomTokenHasReadWriteScope]
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@@ -1378,6 +1272,7 @@ class AccessTokenViewSet(viewsets.ModelViewSet):
|
||||
|
||||
# -------------- DRF custom views --------------------
|
||||
|
||||
|
||||
class AuthTokenThrottle(AnonRateThrottle):
|
||||
rate = '10/day'
|
||||
|
||||
@@ -1390,15 +1285,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({
|
||||
@@ -1428,8 +1322,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()
|
||||
@@ -1437,61 +1330,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)
|
||||
@@ -1510,13 +1382,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)
|
||||
|
||||
@@ -1608,8 +1478,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)
|
||||
|
||||
@@ -1693,8 +1562,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)
|
||||
@@ -1715,14 +1583,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')
|
||||
|
||||
|
||||
@@ -1730,11 +1594,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)
|
||||
|
||||
@@ -1760,9 +1623,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)
|
||||
@@ -1803,12 +1665,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)
|
||||
|
||||
@@ -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):
|
||||
@@ -159,9 +156,7 @@ def recipe_view(request, pk, share=None):
|
||||
|
||||
if request.method == "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')
|
||||
@@ -363,7 +358,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'))
|
||||
|
||||
|
||||
@@ -25,8 +25,7 @@ load_dotenv()
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Get vars from .env files
|
||||
SECRET_KEY = os.getenv('SECRET_KEY') if os.getenv(
|
||||
'SECRET_KEY') else 'INSECURE_STANDARD_KEY_SET_IN_ENV'
|
||||
SECRET_KEY = os.getenv('SECRET_KEY') if os.getenv('SECRET_KEY') else 'INSECURE_STANDARD_KEY_SET_IN_ENV'
|
||||
|
||||
DEBUG = bool(int(os.getenv('DEBUG', True)))
|
||||
DEBUG_TOOLBAR = bool(int(os.getenv('DEBUG_TOOLBAR', True)))
|
||||
@@ -37,11 +36,9 @@ SOCIAL_DEFAULT_GROUP = os.getenv('SOCIAL_DEFAULT_GROUP', 'guest')
|
||||
SPACE_DEFAULT_MAX_RECIPES = int(os.getenv('SPACE_DEFAULT_MAX_RECIPES', 0))
|
||||
SPACE_DEFAULT_MAX_USERS = int(os.getenv('SPACE_DEFAULT_MAX_USERS', 0))
|
||||
SPACE_DEFAULT_MAX_FILES = int(os.getenv('SPACE_DEFAULT_MAX_FILES', 0))
|
||||
SPACE_DEFAULT_ALLOW_SHARING = bool(
|
||||
int(os.getenv('SPACE_DEFAULT_ALLOW_SHARING', True)))
|
||||
SPACE_DEFAULT_ALLOW_SHARING = bool(int(os.getenv('SPACE_DEFAULT_ALLOW_SHARING', True)))
|
||||
|
||||
INTERNAL_IPS = os.getenv('INTERNAL_IPS').split(
|
||||
',') if os.getenv('INTERNAL_IPS') else ['127.0.0.1']
|
||||
INTERNAL_IPS = os.getenv('INTERNAL_IPS').split(',') if os.getenv('INTERNAL_IPS') else ['127.0.0.1']
|
||||
|
||||
# allow djangos wsgi server to server mediafiles
|
||||
GUNICORN_MEDIA = bool(int(os.getenv('GUNICORN_MEDIA', False)))
|
||||
@@ -62,18 +59,15 @@ UNAUTHENTICATED_THEME_FROM_SPACE = int(os.getenv('UNAUTHENTICATED_THEME_FROM_SPA
|
||||
FORCE_THEME_FROM_SPACE = int(os.getenv('FORCE_THEME_FROM_SPACE', 0))
|
||||
|
||||
# minimum interval that users can set for automatic sync of shopping lists
|
||||
SHOPPING_MIN_AUTOSYNC_INTERVAL = int(
|
||||
os.getenv('SHOPPING_MIN_AUTOSYNC_INTERVAL', 5))
|
||||
SHOPPING_MIN_AUTOSYNC_INTERVAL = int(os.getenv('SHOPPING_MIN_AUTOSYNC_INTERVAL', 5))
|
||||
|
||||
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS').split(
|
||||
',') if os.getenv('ALLOWED_HOSTS') else ['*']
|
||||
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS').split(',') if os.getenv('ALLOWED_HOSTS') else ['*']
|
||||
|
||||
if os.getenv('CSRF_TRUSTED_ORIGINS'):
|
||||
CSRF_TRUSTED_ORIGINS = os.getenv('CSRF_TRUSTED_ORIGINS').split(',')
|
||||
|
||||
if CORS_ORIGIN_ALLOW_ALL := os.getenv('CORS_ORIGIN_ALLOW_ALL') is not None:
|
||||
print(
|
||||
'DEPRECATION WARNING: Environment var "CORS_ORIGIN_ALLOW_ALL" is deprecated. Please use "CORS_ALLOW_ALL_ORIGINS."')
|
||||
print('DEPRECATION WARNING: Environment var "CORS_ORIGIN_ALLOW_ALL" is deprecated. Please use "CORS_ALLOW_ALL_ORIGINS."')
|
||||
CORS_ALLOW_ALL_ORIGINS = CORS_ORIGIN_ALLOW_ALL
|
||||
else:
|
||||
CORS_ALLOW_ALL_ORIGINS = bool(int(os.getenv("CORS_ALLOW_ALL_ORIGINS", True)))
|
||||
@@ -105,40 +99,15 @@ PRIVACY_URL = os.getenv('PRIVACY_URL', '')
|
||||
IMPRINT_URL = os.getenv('IMPRINT_URL', '')
|
||||
HOSTED = bool(int(os.getenv('HOSTED', False)))
|
||||
|
||||
MESSAGE_TAGS = {
|
||||
messages.ERROR: 'danger'
|
||||
}
|
||||
MESSAGE_TAGS = {messages.ERROR: 'danger'}
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'dal',
|
||||
'dal_select2',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.sites',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.postgres',
|
||||
'oauth2_provider',
|
||||
'django_prometheus',
|
||||
'django_tables2',
|
||||
'corsheaders',
|
||||
'crispy_forms',
|
||||
'crispy_bootstrap4',
|
||||
'rest_framework',
|
||||
'rest_framework.authtoken',
|
||||
'django_cleanup.apps.CleanupConfig',
|
||||
'webpack_loader',
|
||||
'django_js_reverse',
|
||||
'hcaptcha',
|
||||
'allauth',
|
||||
'allauth.account',
|
||||
'allauth.socialaccount',
|
||||
'cookbook.apps.CookbookConfig',
|
||||
'treebeard',
|
||||
'dal', 'dal_select2', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages',
|
||||
'django.contrib.sites', 'django.contrib.staticfiles', 'django.contrib.postgres', 'oauth2_provider', 'django_prometheus', 'django_tables2', 'corsheaders', 'crispy_forms',
|
||||
'crispy_bootstrap4', 'rest_framework', 'rest_framework.authtoken', 'django_cleanup.apps.CleanupConfig', 'webpack_loader', 'django_js_reverse', 'hcaptcha', 'allauth',
|
||||
'allauth.account', 'allauth.socialaccount', 'cookbook.apps.CookbookConfig', 'treebeard',
|
||||
]
|
||||
|
||||
PLUGINS_DIRECTORY = os.path.join(BASE_DIR, 'recipes', 'plugins')
|
||||
@@ -160,8 +129,7 @@ try:
|
||||
INSTALLED_APPS.append(plugin_module)
|
||||
|
||||
plugin_config = {
|
||||
'name': plugin_class.verbose_name if hasattr(plugin_class,
|
||||
'verbose_name') else plugin_class.name,
|
||||
'name': plugin_class.verbose_name if hasattr(plugin_class, 'verbose_name') else plugin_class.name,
|
||||
'version': plugin_class.VERSION if hasattr(plugin_class, 'VERSION') else 'unknown',
|
||||
'website': plugin_class.website if hasattr(plugin_class, 'website') else '',
|
||||
'github': plugin_class.github if hasattr(plugin_class, 'github') else '',
|
||||
@@ -169,8 +137,7 @@ try:
|
||||
'base_path': os.path.join(BASE_DIR, 'recipes', 'plugins', d),
|
||||
'base_url': plugin_class.base_url,
|
||||
'bundle_name': plugin_class.bundle_name if hasattr(plugin_class, 'bundle_name') else '',
|
||||
'api_router_name': plugin_class.api_router_name if hasattr(plugin_class,
|
||||
'api_router_name') else '',
|
||||
'api_router_name': plugin_class.api_router_name if hasattr(plugin_class, 'api_router_name') else '',
|
||||
'nav_main': plugin_class.nav_main if hasattr(plugin_class, 'nav_main') else '',
|
||||
'nav_dropdown': plugin_class.nav_dropdown if hasattr(plugin_class, 'nav_dropdown') else '',
|
||||
}
|
||||
@@ -184,8 +151,7 @@ except Exception:
|
||||
if DEBUG:
|
||||
print('ERROR failed to initialize plugins')
|
||||
|
||||
SOCIAL_PROVIDERS = os.getenv('SOCIAL_PROVIDERS').split(
|
||||
',') if os.getenv('SOCIAL_PROVIDERS') else []
|
||||
SOCIAL_PROVIDERS = os.getenv('SOCIAL_PROVIDERS').split(',') if os.getenv('SOCIAL_PROVIDERS') else []
|
||||
SOCIALACCOUNT_EMAIL_VERIFICATION = 'none'
|
||||
INSTALLED_APPS = INSTALLED_APPS + SOCIAL_PROVIDERS
|
||||
|
||||
@@ -196,11 +162,9 @@ ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = 90
|
||||
ACCOUNT_LOGOUT_ON_GET = True
|
||||
|
||||
try:
|
||||
SOCIALACCOUNT_PROVIDERS = ast.literal_eval(
|
||||
os.getenv('SOCIALACCOUNT_PROVIDERS') if os.getenv('SOCIALACCOUNT_PROVIDERS') else '{}')
|
||||
SOCIALACCOUNT_PROVIDERS = ast.literal_eval(os.getenv('SOCIALACCOUNT_PROVIDERS') if os.getenv('SOCIALACCOUNT_PROVIDERS') else '{}')
|
||||
except ValueError:
|
||||
SOCIALACCOUNT_PROVIDERS = json.loads(
|
||||
os.getenv('SOCIALACCOUNT_PROVIDERS').replace("'", '"') if os.getenv('SOCIALACCOUNT_PROVIDERS') else '{}')
|
||||
SOCIALACCOUNT_PROVIDERS = json.loads(os.getenv('SOCIALACCOUNT_PROVIDERS').replace("'", '"') if os.getenv('SOCIALACCOUNT_PROVIDERS') else '{}')
|
||||
|
||||
SESSION_COOKIE_DOMAIN = os.getenv('SESSION_COOKIE_DOMAIN', None)
|
||||
SESSION_COOKIE_NAME = os.getenv('SESSION_COOKIE_NAME', 'sessionid')
|
||||
@@ -213,30 +177,21 @@ ENABLE_PDF_EXPORT = bool(int(os.getenv('ENABLE_PDF_EXPORT', False)))
|
||||
EXPORT_FILE_CACHE_DURATION = int(os.getenv('EXPORT_FILE_CACHE_DURATION', 600))
|
||||
|
||||
MIDDLEWARE = [
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'whitenoise.middleware.WhiteNoiseMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'cookbook.helper.scope_middleware.ScopeMiddleware',
|
||||
'allauth.account.middleware.AccountMiddleware',
|
||||
'corsheaders.middleware.CorsMiddleware', 'django.middleware.security.SecurityMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.locale.LocaleMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'cookbook.helper.scope_middleware.ScopeMiddleware', 'allauth.account.middleware.AccountMiddleware',
|
||||
]
|
||||
|
||||
if DEBUG_TOOLBAR:
|
||||
MIDDLEWARE += ('debug_toolbar.middleware.DebugToolbarMiddleware',)
|
||||
INSTALLED_APPS += ('debug_toolbar',)
|
||||
MIDDLEWARE += ('debug_toolbar.middleware.DebugToolbarMiddleware', )
|
||||
INSTALLED_APPS += ('debug_toolbar', )
|
||||
|
||||
SORT_TREE_BY_NAME = bool(int(os.getenv('SORT_TREE_BY_NAME', False)))
|
||||
DISABLE_TREE_FIX_STARTUP = bool(
|
||||
int(os.getenv('DISABLE_TREE_FIX_STARTUP', False)))
|
||||
DISABLE_TREE_FIX_STARTUP = bool(int(os.getenv('DISABLE_TREE_FIX_STARTUP', False)))
|
||||
|
||||
if bool(int(os.getenv('SQL_DEBUG', False))):
|
||||
MIDDLEWARE += ('recipes.middleware.SqlPrintingMiddleware',)
|
||||
MIDDLEWARE += ('recipes.middleware.SqlPrintingMiddleware', )
|
||||
|
||||
if ENABLE_METRICS:
|
||||
MIDDLEWARE += 'django_prometheus.middleware.PrometheusAfterMiddleware',
|
||||
@@ -255,35 +210,34 @@ if LDAP_AUTH:
|
||||
AUTH_LDAP_START_TLS = bool(int(os.getenv('AUTH_LDAP_START_TLS', False)))
|
||||
AUTH_LDAP_BIND_DN = os.getenv('AUTH_LDAP_BIND_DN')
|
||||
AUTH_LDAP_BIND_PASSWORD = os.getenv('AUTH_LDAP_BIND_PASSWORD')
|
||||
AUTH_LDAP_USER_SEARCH = LDAPSearch(
|
||||
os.getenv('AUTH_LDAP_USER_SEARCH_BASE_DN'),
|
||||
ldap.SCOPE_SUBTREE,
|
||||
os.getenv('AUTH_LDAP_USER_SEARCH_FILTER_STR', '(uid=%(user)s)'),
|
||||
)
|
||||
AUTH_LDAP_USER_ATTR_MAP = ast.literal_eval(os.getenv('AUTH_LDAP_USER_ATTR_MAP')) if os.getenv(
|
||||
'AUTH_LDAP_USER_ATTR_MAP') else {
|
||||
AUTH_LDAP_USER_SEARCH = LDAPSearch(os.getenv('AUTH_LDAP_USER_SEARCH_BASE_DN'), ldap.SCOPE_SUBTREE, os.getenv('AUTH_LDAP_USER_SEARCH_FILTER_STR', '(uid=%(user)s)'), )
|
||||
AUTH_LDAP_USER_ATTR_MAP = ast.literal_eval(os.getenv('AUTH_LDAP_USER_ATTR_MAP')) if os.getenv('AUTH_LDAP_USER_ATTR_MAP') else {
|
||||
'first_name': 'givenName',
|
||||
'last_name': 'sn',
|
||||
'email': 'mail',
|
||||
}
|
||||
AUTH_LDAP_ALWAYS_UPDATE_USER = bool(
|
||||
int(os.getenv('AUTH_LDAP_ALWAYS_UPDATE_USER', True)))
|
||||
AUTH_LDAP_ALWAYS_UPDATE_USER = bool(int(os.getenv('AUTH_LDAP_ALWAYS_UPDATE_USER', True)))
|
||||
AUTH_LDAP_CACHE_TIMEOUT = int(os.getenv('AUTH_LDAP_CACHE_TIMEOUT', 3600))
|
||||
if 'AUTH_LDAP_TLS_CACERTFILE' in os.environ:
|
||||
AUTH_LDAP_GLOBAL_OPTIONS = {
|
||||
ldap.OPT_X_TLS_CACERTFILE: os.getenv('AUTH_LDAP_TLS_CACERTFILE')}
|
||||
AUTH_LDAP_GLOBAL_OPTIONS = {ldap.OPT_X_TLS_CACERTFILE: os.getenv('AUTH_LDAP_TLS_CACERTFILE')}
|
||||
if DEBUG:
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"handlers": {"console": {"class": "logging.StreamHandler"}},
|
||||
"loggers": {"django_auth_ldap": {"level": "DEBUG", "handlers": ["console"]}},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler"
|
||||
}
|
||||
},
|
||||
"loggers": {
|
||||
"django_auth_ldap": {
|
||||
"level": "DEBUG",
|
||||
"handlers": ["console"]
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
AUTHENTICATION_BACKENDS += [
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
'allauth.account.auth_backends.AuthenticationBackend',
|
||||
]
|
||||
AUTHENTICATION_BACKENDS += ['django.contrib.auth.backends.ModelBackend', 'allauth.account.auth_backends.AuthenticationBackend', ]
|
||||
|
||||
# django allauth site id
|
||||
SITE_ID = int(os.getenv('ALLAUTH_SITE_ID', 1))
|
||||
@@ -292,75 +246,55 @@ ACCOUNT_ADAPTER = 'cookbook.helper.AllAuthCustomAdapter'
|
||||
|
||||
if REMOTE_USER_AUTH:
|
||||
MIDDLEWARE.insert(8, 'recipes.middleware.CustomRemoteUser')
|
||||
AUTHENTICATION_BACKENDS.append(
|
||||
'django.contrib.auth.backends.RemoteUserBackend')
|
||||
AUTHENTICATION_BACKENDS.append('django.contrib.auth.backends.RemoteUserBackend')
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
AUTH_PASSWORD_VALIDATORS = [{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
}, {
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
}, {
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
}, {
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
}, ]
|
||||
|
||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||
|
||||
OAUTH2_PROVIDER = {
|
||||
'SCOPES': {'read': 'Read scope', 'write': 'Write scope', 'bookmarklet': 'only access to bookmarklet'}
|
||||
}
|
||||
OAUTH2_PROVIDER = {'SCOPES': {'read': 'Read scope', 'write': 'Write scope', 'bookmarklet': 'only access to bookmarklet'}}
|
||||
READ_SCOPE = 'read'
|
||||
WRITE_SCOPE = 'write'
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
|
||||
'rest_framework.authentication.BasicAuthentication',
|
||||
),
|
||||
'DEFAULT_PERMISSION_CLASSES': [
|
||||
'rest_framework.permissions.IsAuthenticated',
|
||||
],
|
||||
'DEFAULT_AUTHENTICATION_CLASSES':
|
||||
('rest_framework.authentication.SessionAuthentication', 'oauth2_provider.contrib.rest_framework.OAuth2Authentication', 'rest_framework.authentication.BasicAuthentication',
|
||||
),
|
||||
'DEFAULT_PERMISSION_CLASSES': ['rest_framework.permissions.IsAuthenticated', ],
|
||||
}
|
||||
|
||||
ROOT_URLCONF = 'recipes.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [os.path.join(BASE_DIR, 'templates'), os.path.join(BASE_DIR, 'cookbook', 'templates')],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
'django.template.context_processors.media',
|
||||
'cookbook.helper.context_processors.context_settings',
|
||||
],
|
||||
},
|
||||
TEMPLATES = [{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [os.path.join(BASE_DIR, 'templates'), os.path.join(BASE_DIR, 'cookbook', 'templates')],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages', 'django.template.context_processors.media', 'cookbook.helper.context_processors.context_settings',
|
||||
],
|
||||
},
|
||||
]
|
||||
}, ]
|
||||
|
||||
WSGI_APPLICATION = 'recipes.wsgi.application'
|
||||
|
||||
# Database
|
||||
# Load settings from env files
|
||||
if os.getenv('DATABASE_URL'):
|
||||
match = re.match(
|
||||
r'(?P<schema>\w+):\/\/(?:(?P<user>[\w\d_-]+)(?::(?P<password>[^@]+))?@)?(?P<host>[^:/]+)(?::(?P<port>\d+))?(?:/(?P<database>[\w\d/._-]+))?',
|
||||
os.getenv('DATABASE_URL')
|
||||
)
|
||||
match = re.match(r'(?P<schema>\w+):\/\/(?:(?P<user>[\w\d_-]+)(?::(?P<password>[^@]+))?@)?(?P<host>[^:/]+)(?::(?P<port>\d+))?(?:/(?P<database>[\w\d/._-]+))?',
|
||||
os.getenv('DATABASE_URL'))
|
||||
settings = match.groupdict()
|
||||
schema = settings['schema']
|
||||
if schema.startswith('postgres'):
|
||||
@@ -421,12 +355,7 @@ else:
|
||||
# }
|
||||
# }
|
||||
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
'LOCATION': 'default',
|
||||
}
|
||||
}
|
||||
CACHES = {'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 'LOCATION': 'default', }}
|
||||
|
||||
# Vue webpack settings
|
||||
VUE_DIR = os.path.join(BASE_DIR, 'vue')
|
||||
@@ -470,33 +399,16 @@ USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
LANGUAGES = [
|
||||
('hy', _('Armenian ')),
|
||||
('bg', _('Bulgarian')),
|
||||
('ca', _('Catalan')),
|
||||
('cs', _('Czech')),
|
||||
('da', _('Danish')),
|
||||
('nl', _('Dutch')),
|
||||
('en', _('English')),
|
||||
('fr', _('French')),
|
||||
('de', _('German')),
|
||||
('hu', _('Hungarian')),
|
||||
('it', _('Italian')),
|
||||
('lv', _('Latvian')),
|
||||
('nb', _('Norwegian ')),
|
||||
('pl', _('Polish')),
|
||||
('ru', _('Russian')),
|
||||
('es', _('Spanish')),
|
||||
('sv', _('Swedish')),
|
||||
]
|
||||
LANGUAGES = [('hy', _('Armenian ')), ('bg', _('Bulgarian')), ('ca', _('Catalan')), ('cs', _('Czech')), ('da', _('Danish')), ('nl', _('Dutch')), ('en', _('English')),
|
||||
('fr', _('French')), ('de', _('German')), ('hu', _('Hungarian')), ('it', _('Italian')), ('lv', _('Latvian')), ('nb', _('Norwegian ')), ('pl', _('Polish')),
|
||||
('ru', _('Russian')), ('es', _('Spanish')), ('sv', _('Swedish')), ]
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/2.0/howto/static-files/
|
||||
|
||||
SCRIPT_NAME = os.getenv('SCRIPT_NAME', '')
|
||||
# path for django_js_reverse to generate the javascript file containing all urls. Only done because the default command (collectstatic_js_reverse) fails to update the manifest
|
||||
JS_REVERSE_OUTPUT_PATH = os.path.join(
|
||||
BASE_DIR, "cookbook/static/django_js_reverse")
|
||||
JS_REVERSE_OUTPUT_PATH = os.path.join(BASE_DIR, "cookbook/static/django_js_reverse")
|
||||
JS_REVERSE_SCRIPT_PREFIX = os.getenv('JS_REVERSE_SCRIPT_PREFIX', SCRIPT_NAME)
|
||||
|
||||
STATIC_URL = os.getenv('STATIC_URL', '/static/')
|
||||
@@ -551,22 +463,12 @@ EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', '')
|
||||
EMAIL_USE_TLS = bool(int(os.getenv('EMAIL_USE_TLS', False)))
|
||||
EMAIL_USE_SSL = bool(int(os.getenv('EMAIL_USE_SSL', False)))
|
||||
DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL', 'webmaster@localhost')
|
||||
ACCOUNT_EMAIL_SUBJECT_PREFIX = os.getenv(
|
||||
'ACCOUNT_EMAIL_SUBJECT_PREFIX', '[Tandoor Recipes] ') # allauth sender prefix
|
||||
ACCOUNT_EMAIL_SUBJECT_PREFIX = os.getenv('ACCOUNT_EMAIL_SUBJECT_PREFIX', '[Tandoor Recipes] ') # allauth sender prefix
|
||||
|
||||
# ACCOUNT_SIGNUP_FORM_CLASS = 'cookbook.forms.AllAuthSignupForm'
|
||||
ACCOUNT_FORMS = {
|
||||
'signup': 'cookbook.forms.AllAuthSignupForm',
|
||||
'reset_password': 'cookbook.forms.CustomPasswordResetForm'
|
||||
}
|
||||
ACCOUNT_FORMS = {'signup': 'cookbook.forms.AllAuthSignupForm', 'reset_password': 'cookbook.forms.CustomPasswordResetForm'}
|
||||
|
||||
ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS = False
|
||||
ACCOUNT_RATE_LIMITS = {
|
||||
"change_password": "1/m/user",
|
||||
"reset_password": "1/m/ip,1/m/key",
|
||||
"reset_password_from_key": "1/m/ip",
|
||||
"signup": "5/m/ip",
|
||||
"login": "5/m/ip",
|
||||
}
|
||||
ACCOUNT_RATE_LIMITS = {"change_password": "1/m/user", "reset_password": "1/m/ip,1/m/key", "reset_password_from_key": "1/m/ip", "signup": "5/m/ip", "login": "5/m/ip", }
|
||||
|
||||
mimetypes.add_type("text/javascript", ".js", True)
|
||||
|
||||
Reference in New Issue
Block a user