Merge branch 'develop' into url_import_recipes
# Conflicts: # cookbook/helper/recipe_url_import.py # cookbook/tests/api/test_api_keyword.py # cookbook/tests/other/test_edits_recipe.py # cookbook/views/api.py # requirements.txt
This commit is contained in:
commit
cc3e00e75f
@ -13,6 +13,7 @@ TIMEZONE=Europe/Berlin
|
|||||||
|
|
||||||
# add only a database password if you want to run with the default postgres, otherwise change settings accordingly
|
# add only a database password if you want to run with the default postgres, otherwise change settings accordingly
|
||||||
DB_ENGINE=django.db.backends.postgresql
|
DB_ENGINE=django.db.backends.postgresql
|
||||||
|
# DB_OPTIONS= {} # e.g. {"sslmode":"require"} to enable ssl
|
||||||
POSTGRES_HOST=db_recipes
|
POSTGRES_HOST=db_recipes
|
||||||
POSTGRES_PORT=5432
|
POSTGRES_PORT=5432
|
||||||
POSTGRES_USER=djangouser
|
POSTGRES_USER=djangouser
|
||||||
|
18
.github/ISSUE_TEMPLATE/help-request.md
vendored
18
.github/ISSUE_TEMPLATE/help-request.md
vendored
@ -6,14 +6,16 @@ labels: setup issue
|
|||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
## Issue
|
||||||
### Version
|
|
||||||
Please provide your current version (can be found on the system page since v0.8.4)
|
|
||||||
Version:
|
|
||||||
|
|
||||||
### Issue
|
|
||||||
Please describe your problem here
|
Please describe your problem here
|
||||||
|
|
||||||
|
|
||||||
|
## Setup Info
|
||||||
|
Version: (can be found on the system page since v0.8.4)
|
||||||
|
OS: e.g. Ubuntu 20.02
|
||||||
|
|
||||||
|
Other relevant information regarding your problem (proxies, firewalls, etc.)
|
||||||
|
|
||||||
### `.env`
|
### `.env`
|
||||||
Please include your `.env` config file (**make sure to remove/replace all secrets**)
|
Please include your `.env` config file (**make sure to remove/replace all secrets**)
|
||||||
```
|
```
|
||||||
@ -25,3 +27,7 @@ When running with docker compose please provide your `docker-compose.yml`
|
|||||||
```
|
```
|
||||||
docker-compose.yml content
|
docker-compose.yml content
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
If you feel like there is anything interesting please post the output of `docker-compose logs` at
|
||||||
|
container startup and when the issue happens.
|
22
.github/ISSUE_TEMPLATE/url_import.md
vendored
Normal file
22
.github/ISSUE_TEMPLATE/url_import.md
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
name: Website Import
|
||||||
|
about: Anything related to website imports
|
||||||
|
title: ''
|
||||||
|
labels: enhancement, url_import
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Version
|
||||||
|
Please provide your current version (can be found on the system page since v0.8.4)
|
||||||
|
Version:
|
||||||
|
|
||||||
|
### Information
|
||||||
|
Exact URL you are trying to import from:
|
||||||
|
|
||||||
|
When did the issue happen: When pressing the search button with the url / when importing after the page has loaded
|
||||||
|
|
||||||
|
Response/Message shown
|
||||||
|
```
|
||||||
|
Message
|
||||||
|
```
|
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -25,4 +25,4 @@ jobs:
|
|||||||
python3 manage.py collectstatic_js_reverse
|
python3 manage.py collectstatic_js_reverse
|
||||||
- name: Django Testing project
|
- name: Django Testing project
|
||||||
run: |
|
run: |
|
||||||
python3 manage.py test
|
pytest
|
||||||
|
@ -29,4 +29,7 @@
|
|||||||
</list>
|
</list>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
|
<component name="TestRunnerService">
|
||||||
|
<option name="PROJECT_TEST_RUNNER" value="pytest" />
|
||||||
|
</component>
|
||||||
</module>
|
</module>
|
@ -1,25 +1,35 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth.admin import UserAdmin
|
||||||
|
from django.contrib.auth.models import User, Group
|
||||||
|
|
||||||
from .models import (Comment, CookLog, Food, Ingredient, InviteLink, Keyword,
|
from .models import (Comment, CookLog, Food, Ingredient, InviteLink, Keyword,
|
||||||
MealPlan, MealType, NutritionInformation, Recipe,
|
MealPlan, MealType, NutritionInformation, Recipe,
|
||||||
RecipeBook, RecipeBookEntry, RecipeImport, ShareLink,
|
RecipeBook, RecipeBookEntry, RecipeImport, ShareLink,
|
||||||
ShoppingList, ShoppingListEntry, ShoppingListRecipe,
|
ShoppingList, ShoppingListEntry, ShoppingListRecipe,
|
||||||
Space, Step, Storage, Sync, SyncLog, Unit, UserPreference,
|
Space, Step, Storage, Sync, SyncLog, Unit, UserPreference,
|
||||||
ViewLog, Supermarket, SupermarketCategory, SupermarketCategoryRelation)
|
ViewLog, Supermarket, SupermarketCategory, SupermarketCategoryRelation, ImportLog)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomUserAdmin(UserAdmin):
|
||||||
|
def has_add_permission(self, request, obj=None):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.unregister(User)
|
||||||
|
admin.site.register(User, CustomUserAdmin)
|
||||||
|
|
||||||
|
admin.site.unregister(Group)
|
||||||
|
|
||||||
|
|
||||||
class SpaceAdmin(admin.ModelAdmin):
|
class SpaceAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name', 'message')
|
list_display = ('name', 'created_by', 'message')
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Space, SpaceAdmin)
|
admin.site.register(Space, SpaceAdmin)
|
||||||
|
|
||||||
|
|
||||||
class UserPreferenceAdmin(admin.ModelAdmin):
|
class UserPreferenceAdmin(admin.ModelAdmin):
|
||||||
list_display = (
|
list_display = ('name', 'space', 'theme', 'nav_color', 'default_page', 'search_style',)
|
||||||
'name', 'theme', 'nav_color',
|
|
||||||
'default_page', 'search_style', 'comments'
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def name(obj):
|
def name(obj):
|
||||||
@ -203,3 +213,10 @@ class NutritionInformationAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
|
|
||||||
admin.site.register(NutritionInformation, NutritionInformationAdmin)
|
admin.site.register(NutritionInformation, NutritionInformationAdmin)
|
||||||
|
|
||||||
|
|
||||||
|
class ImportLogAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('id', 'type', 'running', 'created_by', 'created_at',)
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(ImportLog, ImportLogAdmin)
|
||||||
|
@ -3,25 +3,32 @@ from django.conf import settings
|
|||||||
from django.contrib.postgres.search import TrigramSimilarity
|
from django.contrib.postgres.search import TrigramSimilarity
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from cookbook.forms import MultiSelectWidget
|
from cookbook.forms import MultiSelectWidget
|
||||||
from cookbook.models import Food, Keyword, Recipe, ShoppingList
|
from cookbook.models import Food, Keyword, Recipe, ShoppingList
|
||||||
|
|
||||||
|
with scopes_disabled():
|
||||||
class RecipeFilter(django_filters.FilterSet):
|
class RecipeFilter(django_filters.FilterSet):
|
||||||
name = django_filters.CharFilter(method='filter_name')
|
name = django_filters.CharFilter(method='filter_name')
|
||||||
keywords = django_filters.ModelMultipleChoiceFilter(
|
keywords = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Keyword.objects.all(),
|
queryset=Keyword.objects.none(),
|
||||||
widget=MultiSelectWidget,
|
widget=MultiSelectWidget,
|
||||||
method='filter_keywords'
|
method='filter_keywords'
|
||||||
)
|
)
|
||||||
foods = django_filters.ModelMultipleChoiceFilter(
|
foods = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Food.objects.all(),
|
queryset=Food.objects.none(),
|
||||||
widget=MultiSelectWidget,
|
widget=MultiSelectWidget,
|
||||||
method='filter_foods',
|
method='filter_foods',
|
||||||
label=_('Ingredients')
|
label=_('Ingredients')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __init__(self, data=None, *args, **kwargs):
|
||||||
|
space = kwargs.pop('space')
|
||||||
|
super().__init__(data, *args, **kwargs)
|
||||||
|
self.filters['foods'].queryset = Food.objects.filter(space=space).all()
|
||||||
|
self.filters['keywords'].queryset = Keyword.objects.filter(space=space).all()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def filter_keywords(queryset, name, value):
|
def filter_keywords(queryset, name, value):
|
||||||
if not name == 'keywords':
|
if not name == 'keywords':
|
||||||
@ -35,20 +42,15 @@ class RecipeFilter(django_filters.FilterSet):
|
|||||||
if not name == 'foods':
|
if not name == 'foods':
|
||||||
return queryset
|
return queryset
|
||||||
for x in value:
|
for x in value:
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(steps__ingredients__food__name=x).distinct()
|
||||||
steps__ingredients__food__name=x
|
|
||||||
).distinct()
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def filter_name(queryset, name, value):
|
def filter_name(queryset, name, value):
|
||||||
if not name == 'name':
|
if not name == 'name':
|
||||||
return queryset
|
return queryset
|
||||||
if settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql_psycopg2': # noqa: E501
|
if settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql_psycopg2':
|
||||||
queryset = queryset \
|
queryset = queryset.annotate(similarity=TrigramSimilarity('name', value), ).filter(Q(similarity__gt=0.1) | Q(name__unaccent__icontains=value)).order_by('-similarity')
|
||||||
.annotate(similarity=TrigramSimilarity('name', value), ) \
|
|
||||||
.filter(Q(similarity__gt=0.1) | Q(name__unaccent__icontains=value)) \
|
|
||||||
.order_by('-similarity')
|
|
||||||
else:
|
else:
|
||||||
queryset = queryset.filter(name__icontains=value)
|
queryset = queryset.filter(name__icontains=value)
|
||||||
return queryset
|
return queryset
|
||||||
@ -58,7 +60,7 @@ class RecipeFilter(django_filters.FilterSet):
|
|||||||
fields = ['name', 'keywords', 'foods', 'internal']
|
fields = ['name', 'keywords', 'foods', 'internal']
|
||||||
|
|
||||||
|
|
||||||
class IngredientFilter(django_filters.FilterSet):
|
class FoodFilter(django_filters.FilterSet):
|
||||||
name = django_filters.CharFilter(lookup_expr='icontains')
|
name = django_filters.CharFilter(lookup_expr='icontains')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -72,7 +74,7 @@ class ShoppingListFilter(django_filters.FilterSet):
|
|||||||
if data is not None:
|
if data is not None:
|
||||||
data = data.copy()
|
data = data.copy()
|
||||||
data.setdefault("finished", False)
|
data.setdefault("finished", False)
|
||||||
super(ShoppingListFilter, self).__init__(data, *args, **kwargs)
|
super().__init__(data, *args, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ShoppingList
|
model = ShoppingList
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.forms import widgets
|
from django.forms import widgets
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django_scopes.forms import SafeModelChoiceField, SafeModelMultipleChoiceField
|
||||||
from emoji_picker.widgets import EmojiPickerTextInput
|
from emoji_picker.widgets import EmojiPickerTextInput
|
||||||
|
|
||||||
from .models import (Comment, Food, InviteLink, Keyword, MealPlan, Recipe,
|
from .models import (Comment, Food, InviteLink, Keyword, MealPlan, Recipe,
|
||||||
RecipeBook, RecipeBookEntry, Storage, Sync, Unit, User,
|
RecipeBook, RecipeBookEntry, Storage, Sync, Unit, User,
|
||||||
UserPreference)
|
UserPreference, SupermarketCategory, MealType, Space)
|
||||||
|
|
||||||
|
|
||||||
class SelectWidget(widgets.Select):
|
class SelectWidget(widgets.Select):
|
||||||
@ -74,18 +75,18 @@ class UserNameForm(forms.ModelForm):
|
|||||||
|
|
||||||
class ExternalRecipeForm(forms.ModelForm):
|
class ExternalRecipeForm(forms.ModelForm):
|
||||||
file_path = forms.CharField(disabled=True, required=False)
|
file_path = forms.CharField(disabled=True, required=False)
|
||||||
storage = forms.ModelChoiceField(
|
|
||||||
queryset=Storage.objects.all(),
|
|
||||||
disabled=True,
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
file_uid = forms.CharField(disabled=True, required=False)
|
file_uid = forms.CharField(disabled=True, required=False)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
space = kwargs.pop('space')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['keywords'].queryset = Keyword.objects.filter(space=space).all()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Recipe
|
model = Recipe
|
||||||
fields = (
|
fields = (
|
||||||
'name', 'keywords', 'description', 'servings', 'working_time', 'waiting_time',
|
'name', 'description', 'servings', 'working_time', 'waiting_time',
|
||||||
'file_path', 'storage', 'file_uid'
|
'file_path', 'file_uid', 'keywords'
|
||||||
)
|
)
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
@ -97,38 +98,9 @@ class ExternalRecipeForm(forms.ModelForm):
|
|||||||
'file_uid': _('Storage UID'),
|
'file_uid': _('Storage UID'),
|
||||||
}
|
}
|
||||||
widgets = {'keywords': MultiSelectWidget}
|
widgets = {'keywords': MultiSelectWidget}
|
||||||
|
field_classes = {
|
||||||
|
'keywords': SafeModelMultipleChoiceField,
|
||||||
class InternalRecipeForm(forms.ModelForm):
|
|
||||||
ingredients = forms.CharField(widget=forms.HiddenInput(), required=False)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Recipe
|
|
||||||
fields = (
|
|
||||||
'name', 'image', 'working_time',
|
|
||||||
'waiting_time', 'servings', 'keywords'
|
|
||||||
)
|
|
||||||
|
|
||||||
labels = {
|
|
||||||
'name': _('Name'),
|
|
||||||
'keywords': _('Keywords'),
|
|
||||||
'working_time': _('Preparation time in minutes'),
|
|
||||||
'waiting_time': _('Waiting time (cooking/baking) in minutes'),
|
|
||||||
'servings': _('Number of servings'),
|
|
||||||
}
|
}
|
||||||
widgets = {'keywords': MultiSelectWidget}
|
|
||||||
|
|
||||||
|
|
||||||
class ShoppingForm(forms.Form):
|
|
||||||
recipe = forms.ModelMultipleChoiceField(
|
|
||||||
queryset=Recipe.objects.filter(internal=True).all(),
|
|
||||||
widget=MultiSelectWidget
|
|
||||||
)
|
|
||||||
markdown_format = forms.BooleanField(
|
|
||||||
help_text=_('Include <code>- [ ]</code> in list for easier usage in markdown based documents.'), # noqa: E501
|
|
||||||
required=False,
|
|
||||||
initial=False
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ImportExportBase(forms.Form):
|
class ImportExportBase(forms.Form):
|
||||||
@ -150,42 +122,59 @@ class ImportForm(ImportExportBase):
|
|||||||
|
|
||||||
|
|
||||||
class ExportForm(ImportExportBase):
|
class ExportForm(ImportExportBase):
|
||||||
recipes = forms.ModelMultipleChoiceField(queryset=Recipe.objects.filter(internal=True).all(), widget=MultiSelectWidget)
|
recipes = forms.ModelMultipleChoiceField(widget=MultiSelectWidget, queryset=Recipe.objects.none())
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
space = kwargs.pop('space')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['recipes'].queryset = Recipe.objects.filter(space=space).all()
|
||||||
|
|
||||||
|
|
||||||
class UnitMergeForm(forms.Form):
|
class UnitMergeForm(forms.Form):
|
||||||
prefix = 'unit'
|
prefix = 'unit'
|
||||||
|
|
||||||
new_unit = forms.ModelChoiceField(
|
new_unit = SafeModelChoiceField(
|
||||||
queryset=Unit.objects.all(),
|
queryset=Unit.objects.none(),
|
||||||
widget=SelectWidget,
|
widget=SelectWidget,
|
||||||
label=_('New Unit'),
|
label=_('New Unit'),
|
||||||
help_text=_('New unit that other gets replaced by.'),
|
help_text=_('New unit that other gets replaced by.'),
|
||||||
)
|
)
|
||||||
old_unit = forms.ModelChoiceField(
|
old_unit = SafeModelChoiceField(
|
||||||
queryset=Unit.objects.all(),
|
queryset=Unit.objects.none(),
|
||||||
widget=SelectWidget,
|
widget=SelectWidget,
|
||||||
label=_('Old Unit'),
|
label=_('Old Unit'),
|
||||||
help_text=_('Unit that should be replaced.'),
|
help_text=_('Unit that should be replaced.'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
space = kwargs.pop('space')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['new_unit'].queryset = Unit.objects.filter(space=space).all()
|
||||||
|
self.fields['old_unit'].queryset = Unit.objects.filter(space=space).all()
|
||||||
|
|
||||||
|
|
||||||
class FoodMergeForm(forms.Form):
|
class FoodMergeForm(forms.Form):
|
||||||
prefix = 'food'
|
prefix = 'food'
|
||||||
|
|
||||||
new_food = forms.ModelChoiceField(
|
new_food = SafeModelChoiceField(
|
||||||
queryset=Food.objects.all(),
|
queryset=Food.objects.none(),
|
||||||
widget=SelectWidget,
|
widget=SelectWidget,
|
||||||
label=_('New Food'),
|
label=_('New Food'),
|
||||||
help_text=_('New food that other gets replaced by.'),
|
help_text=_('New food that other gets replaced by.'),
|
||||||
)
|
)
|
||||||
old_food = forms.ModelChoiceField(
|
old_food = SafeModelChoiceField(
|
||||||
queryset=Food.objects.all(),
|
queryset=Food.objects.none(),
|
||||||
widget=SelectWidget,
|
widget=SelectWidget,
|
||||||
label=_('Old Food'),
|
label=_('Old Food'),
|
||||||
help_text=_('Food that should be replaced.'),
|
help_text=_('Food that should be replaced.'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
space = kwargs.pop('space')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['new_food'].queryset = Food.objects.filter(space=space).all()
|
||||||
|
self.fields['old_food'].queryset = Food.objects.filter(space=space).all()
|
||||||
|
|
||||||
|
|
||||||
class CommentForm(forms.ModelForm):
|
class CommentForm(forms.ModelForm):
|
||||||
prefix = 'comment'
|
prefix = 'comment'
|
||||||
@ -210,11 +199,23 @@ class KeywordForm(forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class FoodForm(forms.ModelForm):
|
class FoodForm(forms.ModelForm):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
space = kwargs.pop('space')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['recipe'].queryset = Recipe.objects.filter(space=space).all()
|
||||||
|
self.fields['supermarket_category'].queryset = SupermarketCategory.objects.filter(space=space).all()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Food
|
model = Food
|
||||||
fields = ('name', 'description', 'ignore_shopping', 'recipe', 'supermarket_category')
|
fields = ('name', 'description', 'ignore_shopping', 'recipe', 'supermarket_category')
|
||||||
widgets = {'recipe': SelectWidget}
|
widgets = {'recipe': SelectWidget}
|
||||||
|
|
||||||
|
field_classes = {
|
||||||
|
'recipe': SafeModelChoiceField,
|
||||||
|
'supermarket_category': SafeModelChoiceField,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class StorageForm(forms.ModelForm):
|
class StorageForm(forms.ModelForm):
|
||||||
username = forms.CharField(
|
username = forms.CharField(
|
||||||
@ -222,18 +223,16 @@ class StorageForm(forms.ModelForm):
|
|||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
password = forms.CharField(
|
password = forms.CharField(
|
||||||
widget=forms.TextInput(
|
widget=forms.TextInput(attrs={'autocomplete': 'new-password', 'type': 'password'}),
|
||||||
attrs={'autocomplete': 'new-password', 'type': 'password'}
|
|
||||||
),
|
|
||||||
required=False,
|
required=False,
|
||||||
help_text=_('Leave empty for dropbox and enter app password for nextcloud.') # noqa: E501
|
help_text=_('Leave empty for dropbox and enter app password for nextcloud.')
|
||||||
)
|
)
|
||||||
token = forms.CharField(
|
token = forms.CharField(
|
||||||
widget=forms.TextInput(
|
widget=forms.TextInput(
|
||||||
attrs={'autocomplete': 'new-password', 'type': 'password'}
|
attrs={'autocomplete': 'new-password', 'type': 'password'}
|
||||||
),
|
),
|
||||||
required=False,
|
required=False,
|
||||||
help_text=_('Leave empty for nextcloud and enter api token for dropbox.') # noqa: E501
|
help_text=_('Leave empty for nextcloud and enter api token for dropbox.')
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -241,34 +240,63 @@ class StorageForm(forms.ModelForm):
|
|||||||
fields = ('name', 'method', 'username', 'password', 'token', 'url', 'path')
|
fields = ('name', 'method', 'username', 'password', 'token', 'url', 'path')
|
||||||
|
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'url': _('Leave empty for dropbox and enter only base url for nextcloud (<code>/remote.php/webdav/</code> is added automatically)'), # noqa: E501
|
'url': _('Leave empty for dropbox and enter only base url for nextcloud (<code>/remote.php/webdav/</code> is added automatically)'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class RecipeBookEntryForm(forms.ModelForm):
|
class RecipeBookEntryForm(forms.ModelForm):
|
||||||
prefix = 'bookmark'
|
prefix = 'bookmark'
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
space = kwargs.pop('space')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['book'].queryset = RecipeBook.objects.filter(space=space).all()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RecipeBookEntry
|
model = RecipeBookEntry
|
||||||
fields = ('book',)
|
fields = ('book',)
|
||||||
|
|
||||||
|
field_classes = {
|
||||||
|
'book': SafeModelChoiceField,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class SyncForm(forms.ModelForm):
|
class SyncForm(forms.ModelForm):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
space = kwargs.pop('space')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['storage'].queryset = Storage.objects.filter(space=space).all()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Sync
|
model = Sync
|
||||||
fields = ('storage', 'path', 'active')
|
fields = ('storage', 'path', 'active')
|
||||||
|
|
||||||
|
field_classes = {
|
||||||
|
'storage': SafeModelChoiceField,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class BatchEditForm(forms.Form):
|
class BatchEditForm(forms.Form):
|
||||||
search = forms.CharField(label=_('Search String'))
|
search = forms.CharField(label=_('Search String'))
|
||||||
keywords = forms.ModelMultipleChoiceField(
|
keywords = forms.ModelMultipleChoiceField(
|
||||||
queryset=Keyword.objects.all().order_by('id'),
|
queryset=Keyword.objects.none(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=MultiSelectWidget
|
widget=MultiSelectWidget
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
space = kwargs.pop('space')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['keywords'].queryset = Keyword.objects.filter(space=space).all().order_by('id')
|
||||||
|
|
||||||
|
|
||||||
class ImportRecipeForm(forms.ModelForm):
|
class ImportRecipeForm(forms.ModelForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
space = kwargs.pop('space')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['keywords'].queryset = Keyword.objects.filter(space=space).all()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Recipe
|
model = Recipe
|
||||||
fields = ('name', 'keywords', 'file_path', 'file_uid')
|
fields = ('name', 'keywords', 'file_path', 'file_uid')
|
||||||
@ -280,16 +308,33 @@ class ImportRecipeForm(forms.ModelForm):
|
|||||||
'file_uid': _('File ID'),
|
'file_uid': _('File ID'),
|
||||||
}
|
}
|
||||||
widgets = {'keywords': MultiSelectWidget}
|
widgets = {'keywords': MultiSelectWidget}
|
||||||
|
field_classes = {
|
||||||
|
'keywords': SafeModelChoiceField,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class RecipeBookForm(forms.ModelForm):
|
class RecipeBookForm(forms.ModelForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
space = kwargs.pop('space')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['shared'].queryset = User.objects.filter(userpreference__space=space).all()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RecipeBook
|
model = RecipeBook
|
||||||
fields = ('name', 'icon', 'description', 'shared')
|
fields = ('name', 'icon', 'description', 'shared')
|
||||||
widgets = {'icon': EmojiPickerTextInput, 'shared': MultiSelectWidget}
|
widgets = {'icon': EmojiPickerTextInput, 'shared': MultiSelectWidget}
|
||||||
|
field_classes = {
|
||||||
|
'shared': SafeModelMultipleChoiceField,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class MealPlanForm(forms.ModelForm):
|
class MealPlanForm(forms.ModelForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
space = kwargs.pop('space')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['recipe'].queryset = Recipe.objects.filter(space=space).all()
|
||||||
|
self.fields['meal_type'].queryset = MealType.objects.filter(space=space).all()
|
||||||
|
self.fields['shared'].queryset = User.objects.filter(userpreference__space=space).all()
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
cleaned_data = super(MealPlanForm, self).clean()
|
cleaned_data = super(MealPlanForm, self).clean()
|
||||||
@ -318,15 +363,28 @@ class MealPlanForm(forms.ModelForm):
|
|||||||
'date': DateWidget,
|
'date': DateWidget,
|
||||||
'shared': MultiSelectWidget
|
'shared': MultiSelectWidget
|
||||||
}
|
}
|
||||||
|
field_classes = {
|
||||||
|
'recipe': SafeModelChoiceField,
|
||||||
|
'meal_type': SafeModelChoiceField,
|
||||||
|
'shared': SafeModelMultipleChoiceField,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class InviteLinkForm(forms.ModelForm):
|
class InviteLinkForm(forms.ModelForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
user = kwargs.pop('user')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['space'].queryset = Space.objects.filter(created_by=user).all()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InviteLink
|
model = InviteLink
|
||||||
fields = ('username', 'group', 'valid_until')
|
fields = ('username', 'group', 'valid_until', 'space')
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'username': _('A username is not required, if left blank the new user can choose one.') # noqa: E501
|
'username': _('A username is not required, if left blank the new user can choose one.') # noqa: E501
|
||||||
}
|
}
|
||||||
|
field_classes = {
|
||||||
|
'space': SafeModelChoiceField,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class UserCreateForm(forms.Form):
|
class UserCreateForm(forms.Form):
|
||||||
|
8
cookbook/helper/CustomTestRunner.py
Normal file
8
cookbook/helper/CustomTestRunner.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from django.test.runner import DiscoverRunner
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
|
||||||
|
class CustomTestRunner(DiscoverRunner):
|
||||||
|
def run_tests(self, *args, **kwargs):
|
||||||
|
with scopes_disabled():
|
||||||
|
return super().run_tests(*args, **kwargs)
|
@ -10,7 +10,7 @@ class BaseAutocomplete(autocomplete.Select2QuerySetView):
|
|||||||
if not self.request.user.is_authenticated:
|
if not self.request.user.is_authenticated:
|
||||||
return self.model.objects.none()
|
return self.model.objects.none()
|
||||||
|
|
||||||
qs = self.model.objects.all()
|
qs = self.model.objects.filter(space=self.request.space).all()
|
||||||
|
|
||||||
if self.q:
|
if self.q:
|
||||||
qs = qs.filter(name__icontains=self.q)
|
qs = qs.filter(name__icontains=self.q)
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
"""
|
"""
|
||||||
Source: https://djangosnippets.org/snippets/1703/
|
Source: https://djangosnippets.org/snippets/1703/
|
||||||
"""
|
"""
|
||||||
|
from django.views.generic.detail import SingleObjectTemplateResponseMixin
|
||||||
|
from django.views.generic.edit import ModelFormMixin
|
||||||
|
|
||||||
from cookbook.models import ShareLink
|
from cookbook.models import ShareLink
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import user_passes_test
|
from django.contrib.auth.decorators import user_passes_test
|
||||||
@ -40,8 +43,7 @@ def has_group_permission(user, groups):
|
|||||||
return False
|
return False
|
||||||
groups_allowed = get_allowed_groups(groups)
|
groups_allowed = get_allowed_groups(groups)
|
||||||
if user.is_authenticated:
|
if user.is_authenticated:
|
||||||
if (user.is_superuser
|
if bool(user.groups.filter(name__in=groups_allowed)):
|
||||||
| bool(user.groups.filter(name__in=groups_allowed))):
|
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -56,18 +58,11 @@ def is_object_owner(user, obj):
|
|||||||
:param obj any object that should be tested
|
:param obj any object that should be tested
|
||||||
:return: true if user is owner of object, false otherwise
|
:return: true if user is owner of object, false otherwise
|
||||||
"""
|
"""
|
||||||
# TODO this could be improved/cleaned up by adding
|
|
||||||
# get_owner methods to all models that allow owner checks
|
|
||||||
if not user.is_authenticated:
|
if not user.is_authenticated:
|
||||||
return False
|
return False
|
||||||
if user.is_superuser:
|
try:
|
||||||
return True
|
|
||||||
if owner := getattr(obj, 'created_by', None):
|
|
||||||
return owner == user
|
|
||||||
if owner := getattr(obj, 'user', None):
|
|
||||||
return owner == user
|
|
||||||
if getattr(obj, 'get_owner', None):
|
|
||||||
return obj.get_owner() == user
|
return obj.get_owner() == user
|
||||||
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@ -84,9 +79,7 @@ def is_object_shared(user, obj):
|
|||||||
# share checks for relevant objects
|
# share checks for relevant objects
|
||||||
if not user.is_authenticated:
|
if not user.is_authenticated:
|
||||||
return False
|
return False
|
||||||
if user.is_superuser:
|
return user in obj.get_shared()
|
||||||
return True
|
|
||||||
return user in obj.shared.all()
|
|
||||||
|
|
||||||
|
|
||||||
def share_link_valid(recipe, share):
|
def share_link_valid(recipe, share):
|
||||||
@ -97,11 +90,7 @@ def share_link_valid(recipe, share):
|
|||||||
:return: true if a share link with the given recipe and uuid exists
|
:return: true if a share link with the given recipe and uuid exists
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return (
|
return True if ShareLink.objects.filter(recipe=recipe, uuid=share).exists() else False
|
||||||
True
|
|
||||||
if ShareLink.objects.filter(recipe=recipe, uuid=share).exists()
|
|
||||||
else False
|
|
||||||
)
|
|
||||||
except ValidationError:
|
except ValidationError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -119,7 +108,7 @@ def group_required(*groups_required):
|
|||||||
def in_groups(u):
|
def in_groups(u):
|
||||||
return has_group_permission(u, groups_required)
|
return has_group_permission(u, groups_required)
|
||||||
|
|
||||||
return user_passes_test(in_groups, login_url='view_no_group')
|
return user_passes_test(in_groups, login_url='view_no_perm')
|
||||||
|
|
||||||
|
|
||||||
class GroupRequiredMixin(object):
|
class GroupRequiredMixin(object):
|
||||||
@ -131,13 +120,17 @@ class GroupRequiredMixin(object):
|
|||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
if not has_group_permission(request.user, self.groups_required):
|
if not has_group_permission(request.user, self.groups_required):
|
||||||
messages.add_message(
|
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
|
||||||
request,
|
|
||||||
messages.ERROR,
|
|
||||||
_('You do not have the required permissions to view this page!') # noqa: E501
|
|
||||||
)
|
|
||||||
return HttpResponseRedirect(reverse_lazy('index'))
|
return HttpResponseRedirect(reverse_lazy('index'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
obj = self.get_object()
|
||||||
|
if obj.get_space() != request.space:
|
||||||
|
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
|
||||||
|
return HttpResponseRedirect(reverse_lazy('index'))
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
return super(GroupRequiredMixin, self).dispatch(request, *args, **kwargs)
|
return super(GroupRequiredMixin, self).dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@ -145,25 +138,22 @@ class OwnerRequiredMixin(object):
|
|||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
messages.add_message(
|
messages.add_message(request, messages.ERROR, _('You are not logged in and therefore cannot view this page!'))
|
||||||
request,
|
return HttpResponseRedirect(reverse_lazy('account_login') + '?next=' + request.path)
|
||||||
messages.ERROR,
|
|
||||||
_('You are not logged in and therefore cannot view this page!')
|
|
||||||
)
|
|
||||||
return HttpResponseRedirect(
|
|
||||||
reverse_lazy('account_login') + '?next=' + request.path
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
if not is_object_owner(request.user, self.get_object()):
|
if not is_object_owner(request.user, self.get_object()):
|
||||||
messages.add_message(
|
messages.add_message(request, messages.ERROR, _('You cannot interact with this object as it is not owned by you!'))
|
||||||
request,
|
|
||||||
messages.ERROR,
|
|
||||||
_('You cannot interact with this object as it is not owned by you!') # noqa: E501
|
|
||||||
)
|
|
||||||
return HttpResponseRedirect(reverse('index'))
|
return HttpResponseRedirect(reverse('index'))
|
||||||
|
|
||||||
return super(OwnerRequiredMixin, self) \
|
try:
|
||||||
.dispatch(request, *args, **kwargs)
|
obj = self.get_object()
|
||||||
|
if obj.get_space() != request.space:
|
||||||
|
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
|
||||||
|
return HttpResponseRedirect(reverse_lazy('index'))
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return super(OwnerRequiredMixin, self).dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
# Django Rest Framework Permission classes
|
# Django Rest Framework Permission classes
|
||||||
|
@ -13,7 +13,7 @@ from django.utils.translation import gettext as _
|
|||||||
from recipe_scrapers import _utils
|
from recipe_scrapers import _utils
|
||||||
|
|
||||||
|
|
||||||
def get_from_html(html_text, url):
|
def get_from_html(html_text, url, space):
|
||||||
soup = BeautifulSoup(html_text, "html.parser")
|
soup = BeautifulSoup(html_text, "html.parser")
|
||||||
|
|
||||||
# first try finding ld+json as its most common
|
# first try finding ld+json as its most common
|
||||||
@ -32,7 +32,7 @@ def get_from_html(html_text, url):
|
|||||||
|
|
||||||
if ('@type' in ld_json_item
|
if ('@type' in ld_json_item
|
||||||
and ld_json_item['@type'] == 'Recipe'):
|
and ld_json_item['@type'] == 'Recipe'):
|
||||||
return JsonResponse(find_recipe_json(ld_json_item, url))
|
return JsonResponse(find_recipe_json(ld_json_item, url, space))
|
||||||
except JSONDecodeError:
|
except JSONDecodeError:
|
||||||
return JsonResponse(
|
return JsonResponse(
|
||||||
{
|
{
|
||||||
@ -46,7 +46,7 @@ def get_from_html(html_text, url):
|
|||||||
for i in items:
|
for i in items:
|
||||||
md_json = json.loads(i.json())
|
md_json = json.loads(i.json())
|
||||||
if 'schema.org/Recipe' in str(md_json['type']):
|
if 'schema.org/Recipe' in str(md_json['type']):
|
||||||
return JsonResponse(find_recipe_json(md_json['properties'], url))
|
return JsonResponse(find_recipe_json(md_json['properties'], url, space))
|
||||||
|
|
||||||
return JsonResponse(
|
return JsonResponse(
|
||||||
{
|
{
|
||||||
@ -56,7 +56,7 @@ def get_from_html(html_text, url):
|
|||||||
status=400)
|
status=400)
|
||||||
|
|
||||||
|
|
||||||
def find_recipe_json(ld_json, url):
|
def find_recipe_json(ld_json, url, space):
|
||||||
if type(ld_json['name']) == list:
|
if type(ld_json['name']) == list:
|
||||||
try:
|
try:
|
||||||
ld_json['name'] = ld_json['name'][0]
|
ld_json['name'] = ld_json['name'][0]
|
||||||
@ -85,6 +85,7 @@ def find_recipe_json(ld_json, url):
|
|||||||
|
|
||||||
for x in ld_json['recipeIngredient']:
|
for x in ld_json['recipeIngredient']:
|
||||||
if x.replace(' ', '') != '':
|
if x.replace(' ', '') != '':
|
||||||
|
x = x.replace('½', "0.5").replace('¼', "0.25").replace('¾', "0.75")
|
||||||
try:
|
try:
|
||||||
amount, unit, ingredient, note = parse_ingredient(x)
|
amount, unit, ingredient, note = parse_ingredient(x)
|
||||||
if ingredient:
|
if ingredient:
|
||||||
|
33
cookbook/helper/scope_middleware.py
Normal file
33
cookbook/helper/scope_middleware.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.urls import reverse
|
||||||
|
from django_scopes import scope, scopes_disabled
|
||||||
|
|
||||||
|
from cookbook.views import views
|
||||||
|
|
||||||
|
|
||||||
|
class ScopeMiddleware:
|
||||||
|
def __init__(self, get_response):
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
|
||||||
|
if request.path.startswith('/admin/'):
|
||||||
|
with scopes_disabled():
|
||||||
|
return self.get_response(request)
|
||||||
|
|
||||||
|
with scopes_disabled():
|
||||||
|
if request.user.userpreference.space is None and not reverse('account_logout') in request.path:
|
||||||
|
return views.no_space(request)
|
||||||
|
|
||||||
|
if request.user.groups.count() == 0 and not reverse('account_logout') in request.path:
|
||||||
|
return views.no_groups(request)
|
||||||
|
|
||||||
|
request.space = request.user.userpreference.space
|
||||||
|
# with scopes_disabled():
|
||||||
|
with scope(space=request.space):
|
||||||
|
return self.get_response(request)
|
||||||
|
else:
|
||||||
|
with scopes_disabled():
|
||||||
|
request.space = None
|
||||||
|
return self.get_response(request)
|
@ -1,6 +1,6 @@
|
|||||||
import bleach
|
import bleach
|
||||||
import markdown as md
|
import markdown as md
|
||||||
from bleach_whitelist import markdown_attrs, markdown_tags
|
from bleach_allowlist import markdown_attrs, markdown_tags
|
||||||
from cookbook.helper.mdx_attributes import MarkdownFormatExtension
|
from cookbook.helper.mdx_attributes import MarkdownFormatExtension
|
||||||
from cookbook.helper.mdx_urlize import UrlizeExtension
|
from cookbook.helper.mdx_urlize import UrlizeExtension
|
||||||
from jinja2 import Template, TemplateSyntaxError
|
from jinja2 import Template, TemplateSyntaxError
|
||||||
|
@ -12,7 +12,7 @@ class Chowdown(Integration):
|
|||||||
|
|
||||||
def import_file_name_filter(self, zip_info_object):
|
def import_file_name_filter(self, zip_info_object):
|
||||||
print("testing", zip_info_object.filename)
|
print("testing", zip_info_object.filename)
|
||||||
return re.match(r'^_recipes/([A-Za-z\d\s-])+.md$', zip_info_object.filename)
|
return re.match(r'^(_)*recipes/([A-Za-z\d\s-])+.md$', zip_info_object.filename)
|
||||||
|
|
||||||
def get_recipe_from_file(self, file):
|
def get_recipe_from_file(self, file):
|
||||||
ingredient_mode = False
|
ingredient_mode = False
|
||||||
@ -47,10 +47,10 @@ class Chowdown(Integration):
|
|||||||
if description_mode and len(line) > 3 and '---' not in line:
|
if description_mode and len(line) > 3 and '---' not in line:
|
||||||
descriptions.append(line)
|
descriptions.append(line)
|
||||||
|
|
||||||
recipe = Recipe.objects.create(name=title, created_by=self.request.user, internal=True, )
|
recipe = Recipe.objects.create(name=title, created_by=self.request.user, internal=True, space=self.request.space)
|
||||||
|
|
||||||
for k in tags.split(','):
|
for k in tags.split(','):
|
||||||
keyword, created = Keyword.objects.get_or_create(name=k.strip())
|
keyword, created = Keyword.objects.get_or_create(name=k.strip(), space=self.request.space)
|
||||||
recipe.keywords.add(keyword)
|
recipe.keywords.add(keyword)
|
||||||
|
|
||||||
step = Step.objects.create(
|
step = Step.objects.create(
|
||||||
@ -59,16 +59,16 @@ class Chowdown(Integration):
|
|||||||
|
|
||||||
for ingredient in ingredients:
|
for ingredient in ingredients:
|
||||||
amount, unit, ingredient, note = parse(ingredient)
|
amount, unit, ingredient, note = parse(ingredient)
|
||||||
f, created = Food.objects.get_or_create(name=ingredient)
|
f, created = Food.objects.get_or_create(name=ingredient, space=self.request.space)
|
||||||
u, created = Unit.objects.get_or_create(name=unit)
|
u, created = Unit.objects.get_or_create(name=unit, space=self.request.space)
|
||||||
step.ingredients.add(Ingredient.objects.create(
|
step.ingredients.add(Ingredient.objects.create(
|
||||||
food=f, unit=u, amount=amount, note=note
|
food=f, unit=u, amount=amount, note=note
|
||||||
))
|
))
|
||||||
recipe.steps.add(step)
|
recipe.steps.add(step)
|
||||||
|
|
||||||
for f in self.files:
|
for f in self.files:
|
||||||
if '.zip' in f.name:
|
if '.zip' in f['name']:
|
||||||
import_zip = ZipFile(f.file)
|
import_zip = ZipFile(f['file'])
|
||||||
for z in import_zip.filelist:
|
for z in import_zip.filelist:
|
||||||
if re.match(f'^images/{image}$', z.filename):
|
if re.match(f'^images/{image}$', z.filename):
|
||||||
self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)))
|
self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)))
|
||||||
|
@ -10,7 +10,9 @@ from django.http import HttpResponseRedirect, HttpResponse
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from cookbook.models import Keyword
|
from django_scopes import scope
|
||||||
|
|
||||||
|
from cookbook.models import Keyword, Recipe
|
||||||
|
|
||||||
|
|
||||||
class Integration:
|
class Integration:
|
||||||
@ -18,16 +20,17 @@ class Integration:
|
|||||||
keyword = None
|
keyword = None
|
||||||
files = None
|
files = None
|
||||||
|
|
||||||
def __init__(self, request):
|
def __init__(self, request, export_type):
|
||||||
"""
|
"""
|
||||||
Integration for importing and exporting recipes
|
Integration for importing and exporting recipes
|
||||||
:param request: request context of import session (used to link user to created objects)
|
:param request: request context of import session (used to link user to created objects)
|
||||||
"""
|
"""
|
||||||
self.request = request
|
self.request = request
|
||||||
self.keyword = Keyword.objects.create(
|
self.keyword = Keyword.objects.create(
|
||||||
name=f'Import {date_format(datetime.datetime.now(), "DATETIME_FORMAT")}.{datetime.datetime.now().strftime("%S")}',
|
name=f'Import {export_type} {date_format(datetime.datetime.now(), "DATETIME_FORMAT")}.{datetime.datetime.now().strftime("%S")}',
|
||||||
description=f'Imported by {request.user.get_user_name()} at {date_format(datetime.datetime.now(), "DATETIME_FORMAT")}',
|
description=f'Imported by {request.user.get_user_name()} at {date_format(datetime.datetime.now(), "DATETIME_FORMAT")}',
|
||||||
icon='📥'
|
icon='📥',
|
||||||
|
space=request.space
|
||||||
)
|
)
|
||||||
|
|
||||||
def do_export(self, recipes):
|
def do_export(self, recipes):
|
||||||
@ -40,7 +43,7 @@ class Integration:
|
|||||||
export_zip_obj = ZipFile(export_zip_stream, 'w')
|
export_zip_obj = ZipFile(export_zip_stream, 'w')
|
||||||
|
|
||||||
for r in recipes:
|
for r in recipes:
|
||||||
if r.internal:
|
if r.internal and r.space == self.request.space:
|
||||||
recipe_zip_stream = BytesIO()
|
recipe_zip_stream = BytesIO()
|
||||||
recipe_zip_obj = ZipFile(recipe_zip_stream, 'w')
|
recipe_zip_obj = ZipFile(recipe_zip_stream, 'w')
|
||||||
|
|
||||||
@ -74,29 +77,55 @@ class Integration:
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def do_import(self, files):
|
def do_import(self, files, il):
|
||||||
"""
|
"""
|
||||||
Imports given files
|
Imports given files
|
||||||
:param files: List of in memory files
|
:param files: List of in memory files
|
||||||
|
:param il: Import Log object to refresh while running
|
||||||
:return: HttpResponseRedirect to the recipe search showing all imported recipes
|
:return: HttpResponseRedirect to the recipe search showing all imported recipes
|
||||||
"""
|
"""
|
||||||
|
with scope(space=self.request.space):
|
||||||
|
ignored_recipes = []
|
||||||
try:
|
try:
|
||||||
self.files = files
|
self.files = files
|
||||||
for f in files:
|
for f in files:
|
||||||
if '.zip' in f.name:
|
if '.zip' in f['name'] or '.paprikarecipes' in f['name']:
|
||||||
import_zip = ZipFile(f.file)
|
import_zip = ZipFile(f['file'])
|
||||||
for z in import_zip.filelist:
|
for z in import_zip.filelist:
|
||||||
if self.import_file_name_filter(z):
|
if self.import_file_name_filter(z):
|
||||||
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
|
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
|
||||||
recipe.keywords.add(self.keyword)
|
recipe.keywords.add(self.keyword)
|
||||||
|
il.msg += f'{recipe.pk} - {recipe.name} \n'
|
||||||
|
if duplicate := self.is_duplicate(recipe):
|
||||||
|
ignored_recipes.append(duplicate)
|
||||||
import_zip.close()
|
import_zip.close()
|
||||||
else:
|
else:
|
||||||
recipe = self.get_recipe_from_file(f.file)
|
recipe = self.get_recipe_from_file(f['file'])
|
||||||
recipe.keywords.add(self.keyword)
|
recipe.keywords.add(self.keyword)
|
||||||
|
il.msg += f'{recipe.pk} - {recipe.name} \n'
|
||||||
|
if duplicate := self.is_duplicate(recipe):
|
||||||
|
ignored_recipes.append(duplicate)
|
||||||
except BadZipFile:
|
except BadZipFile:
|
||||||
messages.add_message(self.request, messages.ERROR, _('Importer expected a .zip file. Did you choose the correct importer type for your data ?'))
|
il.msg += 'ERROR ' + _('Importer expected a .zip file. Did you choose the correct importer type for your data ?') + '\n'
|
||||||
|
|
||||||
return HttpResponseRedirect(reverse('view_search') + '?keywords=' + str(self.keyword.pk))
|
if len(ignored_recipes) > 0:
|
||||||
|
il.msg += '\n' + _('The following recipes were ignored because they already existed:') + ' ' + ', '.join(ignored_recipes) + '\n\n'
|
||||||
|
|
||||||
|
il.keyword = self.keyword
|
||||||
|
il.msg += (_('Imported %s recipes.') % Recipe.objects.filter(keywords=self.keyword).count()) + '\n'
|
||||||
|
il.running = False
|
||||||
|
il.save()
|
||||||
|
|
||||||
|
def is_duplicate(self, recipe):
|
||||||
|
"""
|
||||||
|
Checks if a recipe is already present, if so deletes it
|
||||||
|
:param recipe: Recipe object
|
||||||
|
"""
|
||||||
|
if Recipe.objects.filter(space=self.request.space, name=recipe.name).count() > 1:
|
||||||
|
recipe.delete()
|
||||||
|
return recipe.name
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def import_recipe_image(recipe, image_file):
|
def import_recipe_image(recipe, image_file):
|
||||||
|
@ -18,7 +18,7 @@ class Mealie(Integration):
|
|||||||
|
|
||||||
recipe = Recipe.objects.create(
|
recipe = Recipe.objects.create(
|
||||||
name=recipe_json['name'].strip(), description=recipe_json['description'].strip(),
|
name=recipe_json['name'].strip(), description=recipe_json['description'].strip(),
|
||||||
created_by=self.request.user, internal=True)
|
created_by=self.request.user, internal=True, space=self.request.space)
|
||||||
|
|
||||||
# TODO parse times (given in PT2H3M )
|
# TODO parse times (given in PT2H3M )
|
||||||
|
|
||||||
@ -32,16 +32,16 @@ class Mealie(Integration):
|
|||||||
|
|
||||||
for ingredient in recipe_json['recipeIngredient']:
|
for ingredient in recipe_json['recipeIngredient']:
|
||||||
amount, unit, ingredient, note = parse(ingredient)
|
amount, unit, ingredient, note = parse(ingredient)
|
||||||
f, created = Food.objects.get_or_create(name=ingredient)
|
f, created = Food.objects.get_or_create(name=ingredient, space=self.request.space)
|
||||||
u, created = Unit.objects.get_or_create(name=unit)
|
u, created = Unit.objects.get_or_create(name=unit, space=self.request.space)
|
||||||
step.ingredients.add(Ingredient.objects.create(
|
step.ingredients.add(Ingredient.objects.create(
|
||||||
food=f, unit=u, amount=amount, note=note
|
food=f, unit=u, amount=amount, note=note
|
||||||
))
|
))
|
||||||
recipe.steps.add(step)
|
recipe.steps.add(step)
|
||||||
|
|
||||||
for f in self.files:
|
for f in self.files:
|
||||||
if '.zip' in f.name:
|
if '.zip' in f['name']:
|
||||||
import_zip = ZipFile(f.file)
|
import_zip = ZipFile(f['file'])
|
||||||
for z in import_zip.filelist:
|
for z in import_zip.filelist:
|
||||||
if re.match(f'^images/{recipe_json["slug"]}.jpg$', z.filename):
|
if re.match(f'^images/{recipe_json["slug"]}.jpg$', z.filename):
|
||||||
self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)))
|
self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)))
|
||||||
|
@ -19,7 +19,7 @@ class NextcloudCookbook(Integration):
|
|||||||
recipe = Recipe.objects.create(
|
recipe = Recipe.objects.create(
|
||||||
name=recipe_json['name'].strip(), description=recipe_json['description'].strip(),
|
name=recipe_json['name'].strip(), description=recipe_json['description'].strip(),
|
||||||
created_by=self.request.user, internal=True,
|
created_by=self.request.user, internal=True,
|
||||||
servings=recipe_json['recipeYield'])
|
servings=recipe_json['recipeYield'], space=self.request.space)
|
||||||
|
|
||||||
# TODO parse times (given in PT2H3M )
|
# TODO parse times (given in PT2H3M )
|
||||||
# TODO parse keywords
|
# TODO parse keywords
|
||||||
@ -34,16 +34,16 @@ class NextcloudCookbook(Integration):
|
|||||||
|
|
||||||
for ingredient in recipe_json['recipeIngredient']:
|
for ingredient in recipe_json['recipeIngredient']:
|
||||||
amount, unit, ingredient, note = parse(ingredient)
|
amount, unit, ingredient, note = parse(ingredient)
|
||||||
f, created = Food.objects.get_or_create(name=ingredient)
|
f, created = Food.objects.get_or_create(name=ingredient, space=self.request.space)
|
||||||
u, created = Unit.objects.get_or_create(name=unit)
|
u, created = Unit.objects.get_or_create(name=unit, space=self.request.space)
|
||||||
step.ingredients.add(Ingredient.objects.create(
|
step.ingredients.add(Ingredient.objects.create(
|
||||||
food=f, unit=u, amount=amount, note=note
|
food=f, unit=u, amount=amount, note=note
|
||||||
))
|
))
|
||||||
recipe.steps.add(step)
|
recipe.steps.add(step)
|
||||||
|
|
||||||
for f in self.files:
|
for f in self.files:
|
||||||
if '.zip' in f.name:
|
if '.zip' in f['name']:
|
||||||
import_zip = ZipFile(f.file)
|
import_zip = ZipFile(f['file'])
|
||||||
for z in import_zip.filelist:
|
for z in import_zip.filelist:
|
||||||
if re.match(f'^Recipes/{recipe.name}/full.jpg$', z.filename):
|
if re.match(f'^Recipes/{recipe.name}/full.jpg$', z.filename):
|
||||||
self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)))
|
self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)))
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import base64
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
@ -6,51 +7,39 @@ from zipfile import ZipFile
|
|||||||
import microdata
|
import microdata
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
from cookbook.helper.ingredient_parser import parse
|
||||||
from cookbook.helper.recipe_url_import import find_recipe_json
|
from cookbook.helper.recipe_url_import import find_recipe_json
|
||||||
from cookbook.integration.integration import Integration
|
from cookbook.integration.integration import Integration
|
||||||
from cookbook.models import Recipe, Step, Food, Ingredient, Unit
|
from cookbook.models import Recipe, Step, Food, Ingredient, Unit
|
||||||
|
import gzip
|
||||||
|
|
||||||
|
|
||||||
class Paprika(Integration):
|
class Paprika(Integration):
|
||||||
|
|
||||||
def import_file_name_filter(self, zip_info_object):
|
|
||||||
print("testing", zip_info_object.filename)
|
|
||||||
return re.match(r'^Recipes/([A-Za-z\s])+.html$', zip_info_object.filename)
|
|
||||||
|
|
||||||
def get_file_from_recipe(self, recipe):
|
def get_file_from_recipe(self, recipe):
|
||||||
raise NotImplementedError('Method not implemented in storage integration')
|
raise NotImplementedError('Method not implemented in storage integration')
|
||||||
|
|
||||||
def get_recipe_from_file(self, file):
|
def get_recipe_from_file(self, file):
|
||||||
html_text = file.getvalue().decode("utf-8")
|
with gzip.open(file, 'r') as recipe_zip:
|
||||||
|
recipe_json = json.loads(recipe_zip.read().decode("utf-8"))
|
||||||
|
|
||||||
|
recipe = Recipe.objects.create(
|
||||||
|
name=recipe_json['name'].strip(), description=recipe_json['description'].strip(),
|
||||||
|
created_by=self.request.user, internal=True, space=self.request.space)
|
||||||
|
|
||||||
items = microdata.get_items(html_text)
|
|
||||||
for i in items:
|
|
||||||
md_json = json.loads(i.json())
|
|
||||||
if 'schema.org/Recipe' in str(md_json['type']):
|
|
||||||
recipe_json = find_recipe_json(md_json['properties'], '')
|
|
||||||
recipe = Recipe.objects.create(name=recipe_json['name'].strip(), created_by=self.request.user, internal=True)
|
|
||||||
step = Step.objects.create(
|
step = Step.objects.create(
|
||||||
instruction=recipe_json['recipeInstructions']
|
instruction=recipe_json['directions'] + '\n\n' + recipe_json['nutritional_info']
|
||||||
)
|
)
|
||||||
|
|
||||||
for ingredient in recipe_json['recipeIngredient']:
|
for ingredient in recipe_json['ingredients'].split('\n'):
|
||||||
f, created = Food.objects.get_or_create(name=ingredient['ingredient']['text'])
|
amount, unit, ingredient, note = parse(ingredient)
|
||||||
u, created = Unit.objects.get_or_create(name=ingredient['unit']['text'])
|
f, created = Food.objects.get_or_create(name=ingredient, space=self.request.space)
|
||||||
|
u, created = Unit.objects.get_or_create(name=unit, space=self.request.space)
|
||||||
step.ingredients.add(Ingredient.objects.create(
|
step.ingredients.add(Ingredient.objects.create(
|
||||||
food=f, unit=u, amount=ingredient['amount'], note=ingredient['note']
|
food=f, unit=u, amount=amount, note=note
|
||||||
))
|
))
|
||||||
|
|
||||||
recipe.steps.add(step)
|
recipe.steps.add(step)
|
||||||
|
|
||||||
soup = BeautifulSoup(html_text, "html.parser")
|
self.import_recipe_image(recipe, BytesIO(base64.b64decode(recipe_json['photo_data'])))
|
||||||
image = soup.find('img')
|
|
||||||
image_name = image.attrs['src'].strip().replace('Images/', '')
|
|
||||||
|
|
||||||
for f in self.files:
|
|
||||||
if '.zip' in f.name:
|
|
||||||
import_zip = ZipFile(f.file)
|
|
||||||
for z in import_zip.filelist:
|
|
||||||
if re.match(f'^Recipes/Images/{image_name}$', z.filename):
|
|
||||||
self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)))
|
|
||||||
|
|
||||||
return recipe
|
return recipe
|
||||||
|
@ -41,14 +41,14 @@ class Safron(Integration):
|
|||||||
ingredient_mode = False
|
ingredient_mode = False
|
||||||
direction_mode = True
|
direction_mode = True
|
||||||
|
|
||||||
recipe = Recipe.objects.create(name=title, description=description, created_by=self.request.user, internal=True, )
|
recipe = Recipe.objects.create(name=title, description=description, created_by=self.request.user, internal=True, space=self.request.space, )
|
||||||
|
|
||||||
step = Step.objects.create(instruction='\n'.join(directions))
|
step = Step.objects.create(instruction='\n'.join(directions))
|
||||||
|
|
||||||
for ingredient in ingredients:
|
for ingredient in ingredients:
|
||||||
amount, unit, ingredient, note = parse(ingredient)
|
amount, unit, ingredient, note = parse(ingredient)
|
||||||
f, created = Food.objects.get_or_create(name=ingredient)
|
f, created = Food.objects.get_or_create(name=ingredient, space=self.request.space)
|
||||||
u, created = Unit.objects.get_or_create(name=unit)
|
u, created = Unit.objects.get_or_create(name=unit, space=self.request.space)
|
||||||
step.ingredients.add(Ingredient.objects.create(
|
step.ingredients.add(Ingredient.objects.create(
|
||||||
food=f, unit=u, amount=amount, note=note
|
food=f, unit=u, amount=amount, note=note
|
||||||
))
|
))
|
||||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
cookbook/locale/hy/LC_MESSAGES/django.mo
Normal file
BIN
cookbook/locale/hy/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
1880
cookbook/locale/hy/LC_MESSAGES/django.po
Normal file
1880
cookbook/locale/hy/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -15,12 +15,11 @@ msgstr ""
|
|||||||
"POT-Creation-Date: 2021-02-09 18:01+0100\n"
|
"POT-Creation-Date: 2021-02-09 18:01+0100\n"
|
||||||
"PO-Revision-Date: 2020-06-02 19:28+0000\n"
|
"PO-Revision-Date: 2020-06-02 19:28+0000\n"
|
||||||
"Last-Translator: Oliver Thomas Cervera <cervera93-10@yahoo.it>, 2021\n"
|
"Last-Translator: Oliver Thomas Cervera <cervera93-10@yahoo.it>, 2021\n"
|
||||||
"Language-Team: Italian (https://www.transifex.com/django-recipes/"
|
"Language-Team: Italian (https://www.transifex.com/django-recipes/teams/110507/it/)\n"
|
||||||
"teams/110507/it/)\n"
|
|
||||||
"Language: it\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Language: it\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
#: .\cookbook\filters.py:22 .\cookbook\templates\base.html:87
|
#: .\cookbook\filters.py:22 .\cookbook\templates\base.html:87
|
||||||
@ -84,12 +83,12 @@ msgstr ""
|
|||||||
"visualizza una lista della spesa, la lista viene aggiornata ogni tot secondi"
|
"visualizza una lista della spesa, la lista viene aggiornata ogni tot secondi"
|
||||||
" impostati per sincronizzare le modifiche che qualcun altro potrebbe aver "
|
" impostati per sincronizzare le modifiche che qualcun altro potrebbe aver "
|
||||||
"fatto. Utile per gli acquisti con più persone, ma potrebbe utilizzare un po'"
|
"fatto. Utile per gli acquisti con più persone, ma potrebbe utilizzare un po'"
|
||||||
"di dati mobili. Se inferiore al limite di istanza viene ripristinato durante "
|
" di dati mobili. Se inferiore al limite di istanza viene ripristinato "
|
||||||
"il salvataggio."
|
"durante il salvataggio."
|
||||||
|
|
||||||
#: .\cookbook\forms.py:55
|
#: .\cookbook\forms.py:55
|
||||||
msgid "Makes the navbar stick to the top of the page."
|
msgid "Makes the navbar stick to the top of the page."
|
||||||
msgstr ""
|
msgstr "Fissa la barra di navigazione nella parte superiore della pagina."
|
||||||
|
|
||||||
#: .\cookbook\forms.py:71
|
#: .\cookbook\forms.py:71
|
||||||
msgid ""
|
msgid ""
|
||||||
@ -128,10 +127,8 @@ msgid "Storage UID"
|
|||||||
msgstr "UID di archiviazione"
|
msgstr "UID di archiviazione"
|
||||||
|
|
||||||
#: .\cookbook\forms.py:117
|
#: .\cookbook\forms.py:117
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Number of Days"
|
|
||||||
msgid "Number of servings"
|
msgid "Number of servings"
|
||||||
msgstr "Numero di giorni"
|
msgstr "Porzioni"
|
||||||
|
|
||||||
#: .\cookbook\forms.py:128
|
#: .\cookbook\forms.py:128
|
||||||
msgid ""
|
msgid ""
|
||||||
@ -143,7 +140,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: .\cookbook\forms.py:143
|
#: .\cookbook\forms.py:143
|
||||||
msgid "Default"
|
msgid "Default"
|
||||||
msgstr ""
|
msgstr "Predefinito"
|
||||||
|
|
||||||
#: .\cookbook\forms.py:162
|
#: .\cookbook\forms.py:162
|
||||||
msgid "New Unit"
|
msgid "New Unit"
|
||||||
@ -192,11 +189,11 @@ msgstr "Lascia vuoto per nextcloud e inserisci l'api token per dropbox."
|
|||||||
|
|
||||||
#: .\cookbook\forms.py:244
|
#: .\cookbook\forms.py:244
|
||||||
msgid ""
|
msgid ""
|
||||||
"Leave empty for dropbox and enter only base url for nextcloud (<code>/remote."
|
"Leave empty for dropbox and enter only base url for nextcloud "
|
||||||
"php/webdav/</code> is added automatically)"
|
"(<code>/remote.php/webdav/</code> is added automatically)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Lascia vuoto per dropbox e inserisci solo l'url base per nextcloud (<code>/"
|
"Lascia vuoto per dropbox e inserisci solo l'url base per nextcloud "
|
||||||
"remote.php/webdav/</code> è aggiunto automaticamente)"
|
"(<code>/remote.php/webdav/</code> è aggiunto automaticamente)"
|
||||||
|
|
||||||
#: .\cookbook\forms.py:263
|
#: .\cookbook\forms.py:263
|
||||||
msgid "Search String"
|
msgid "Search String"
|
||||||
@ -219,11 +216,11 @@ msgstr ""
|
|||||||
#: .\cookbook\forms.py:313
|
#: .\cookbook\forms.py:313
|
||||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:377
|
#: .\cookbook\templates\forms\edit_internal_recipe.html:377
|
||||||
msgid ""
|
msgid ""
|
||||||
"You can use markdown to format this field. See the <a href=\"/docs/markdown/"
|
"You can use markdown to format this field. See the <a "
|
||||||
"\">docs here</a>"
|
"href=\"/docs/markdown/\">docs here</a>"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Puoi usare markdown per formattare questo campo. Guarda la <a href=\"/docs/"
|
"Puoi usare markdown per formattare questo campo. Guarda la <a "
|
||||||
"markdown/\">documentazione qui</a>"
|
"href=\"/docs/markdown/\">documentazione qui</a>"
|
||||||
|
|
||||||
#: .\cookbook\forms.py:328
|
#: .\cookbook\forms.py:328
|
||||||
msgid "A username is not required, if left blank the new user can choose one."
|
msgid "A username is not required, if left blank the new user can choose one."
|
||||||
@ -238,11 +235,11 @@ msgstr ""
|
|||||||
#: .\cookbook\helper\permission_helper.py:242 .\cookbook\views\data.py:32
|
#: .\cookbook\helper\permission_helper.py:242 .\cookbook\views\data.py:32
|
||||||
#: .\cookbook\views\views.py:106 .\cookbook\views\views.py:218
|
#: .\cookbook\views\views.py:106 .\cookbook\views\views.py:218
|
||||||
msgid "You do not have the required permissions to view this page!"
|
msgid "You do not have the required permissions to view this page!"
|
||||||
msgstr "Non hai i permessi richiesti per visualizzare questa pagina!"
|
msgstr "Non hai i permessi necessari per visualizzare questa pagina!"
|
||||||
|
|
||||||
#: .\cookbook\helper\permission_helper.py:151
|
#: .\cookbook\helper\permission_helper.py:151
|
||||||
msgid "You are not logged in and therefore cannot view this page!"
|
msgid "You are not logged in and therefore cannot view this page!"
|
||||||
msgstr "Non sei loggato e quindi non puoi visualizzare questa pagina!"
|
msgstr "Non hai fatto l'accesso e quindi non puoi visualizzare questa pagina!"
|
||||||
|
|
||||||
#: .\cookbook\helper\permission_helper.py:161
|
#: .\cookbook\helper\permission_helper.py:161
|
||||||
#: .\cookbook\helper\permission_helper.py:177
|
#: .\cookbook\helper\permission_helper.py:177
|
||||||
@ -273,6 +270,8 @@ msgid ""
|
|||||||
"Importer expected a .zip file. Did you choose the correct importer type for "
|
"Importer expected a .zip file. Did you choose the correct importer type for "
|
||||||
"your data ?"
|
"your data ?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"La procedura di import necessita di un file .zip. Hai scelto il tipo di "
|
||||||
|
"importazione corretta per i tuoi dati?"
|
||||||
|
|
||||||
#: .\cookbook\integration\safron.py:23
|
#: .\cookbook\integration\safron.py:23
|
||||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:65
|
#: .\cookbook\templates\forms\edit_internal_recipe.html:65
|
||||||
@ -282,10 +281,8 @@ msgid "Servings"
|
|||||||
msgstr "Porzioni"
|
msgstr "Porzioni"
|
||||||
|
|
||||||
#: .\cookbook\integration\safron.py:25
|
#: .\cookbook\integration\safron.py:25
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Waiting time ~"
|
|
||||||
msgid "Waiting time"
|
msgid "Waiting time"
|
||||||
msgstr "Tempo di Attesa ~"
|
msgstr "Tempo di cottura"
|
||||||
|
|
||||||
#: .\cookbook\integration\safron.py:27
|
#: .\cookbook\integration\safron.py:27
|
||||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:59
|
#: .\cookbook\templates\forms\edit_internal_recipe.html:59
|
||||||
@ -300,7 +297,7 @@ msgstr "Ricettario"
|
|||||||
|
|
||||||
#: .\cookbook\integration\safron.py:31
|
#: .\cookbook\integration\safron.py:31
|
||||||
msgid "Section"
|
msgid "Section"
|
||||||
msgstr ""
|
msgstr "Selezione"
|
||||||
|
|
||||||
#: .\cookbook\migrations\0047_auto_20200602_1133.py:12
|
#: .\cookbook\migrations\0047_auto_20200602_1133.py:12
|
||||||
msgid "Breakfast"
|
msgid "Breakfast"
|
||||||
@ -396,37 +393,35 @@ msgstr "Login"
|
|||||||
#: .\cookbook\templates\account\login.html:13
|
#: .\cookbook\templates\account\login.html:13
|
||||||
#: .\cookbook\templates\account\login.html:28
|
#: .\cookbook\templates\account\login.html:28
|
||||||
msgid "Sign In"
|
msgid "Sign In"
|
||||||
msgstr ""
|
msgstr "Accedi"
|
||||||
|
|
||||||
#: .\cookbook\templates\account\login.html:38
|
#: .\cookbook\templates\account\login.html:38
|
||||||
msgid "Social Login"
|
msgid "Social Login"
|
||||||
msgstr ""
|
msgstr "Login con social network"
|
||||||
|
|
||||||
#: .\cookbook\templates\account\login.html:39
|
#: .\cookbook\templates\account\login.html:39
|
||||||
msgid "You can use any of the following providers to sign in."
|
msgid "You can use any of the following providers to sign in."
|
||||||
msgstr ""
|
msgstr "Puoi usare uno dei seguenti provider per accedere."
|
||||||
|
|
||||||
#: .\cookbook\templates\account\logout.html:5
|
#: .\cookbook\templates\account\logout.html:5
|
||||||
#: .\cookbook\templates\account\logout.html:9
|
#: .\cookbook\templates\account\logout.html:9
|
||||||
#: .\cookbook\templates\account\logout.html:18
|
#: .\cookbook\templates\account\logout.html:18
|
||||||
msgid "Sign Out"
|
msgid "Sign Out"
|
||||||
msgstr ""
|
msgstr "Esci"
|
||||||
|
|
||||||
#: .\cookbook\templates\account\logout.html:11
|
#: .\cookbook\templates\account\logout.html:11
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Are you sure that you want to merge these two units?"
|
|
||||||
msgid "Are you sure you want to sign out?"
|
msgid "Are you sure you want to sign out?"
|
||||||
msgstr "Sei sicuro di volere unire queste due unità di misura?"
|
msgstr "Sei sicuro di voler uscire?"
|
||||||
|
|
||||||
#: .\cookbook\templates\account\password_reset.html:5
|
#: .\cookbook\templates\account\password_reset.html:5
|
||||||
#: .\cookbook\templates\account\password_reset_done.html:5
|
#: .\cookbook\templates\account\password_reset_done.html:5
|
||||||
msgid "Password Reset"
|
msgid "Password Reset"
|
||||||
msgstr ""
|
msgstr "Recupero password"
|
||||||
|
|
||||||
#: .\cookbook\templates\account\password_reset.html:9
|
#: .\cookbook\templates\account\password_reset.html:9
|
||||||
#: .\cookbook\templates\account\password_reset_done.html:9
|
#: .\cookbook\templates\account\password_reset_done.html:9
|
||||||
msgid "Password reset is not implemented for the time being!"
|
msgid "Password reset is not implemented for the time being!"
|
||||||
msgstr ""
|
msgstr "Il recupero della password non è stato ancora implementato!"
|
||||||
|
|
||||||
#: .\cookbook\templates\account\signup.html:5
|
#: .\cookbook\templates\account\signup.html:5
|
||||||
msgid "Register"
|
msgid "Register"
|
||||||
@ -461,7 +456,7 @@ msgstr "Parola chiave"
|
|||||||
|
|
||||||
#: .\cookbook\templates\base.html:100
|
#: .\cookbook\templates\base.html:100
|
||||||
msgid "Batch Edit"
|
msgid "Batch Edit"
|
||||||
msgstr "Modifica di massa"
|
msgstr "Modifica in blocco"
|
||||||
|
|
||||||
#: .\cookbook\templates\base.html:105
|
#: .\cookbook\templates\base.html:105
|
||||||
msgid "Storage Data"
|
msgid "Storage Data"
|
||||||
@ -531,17 +526,17 @@ msgstr "Logout"
|
|||||||
|
|
||||||
#: .\cookbook\templates\batch\edit.html:6
|
#: .\cookbook\templates\batch\edit.html:6
|
||||||
msgid "Batch edit Category"
|
msgid "Batch edit Category"
|
||||||
msgstr "Modifica di massa per categoria"
|
msgstr "Modifica in blocco per categoria"
|
||||||
|
|
||||||
#: .\cookbook\templates\batch\edit.html:15
|
#: .\cookbook\templates\batch\edit.html:15
|
||||||
msgid "Batch edit Recipes"
|
msgid "Batch edit Recipes"
|
||||||
msgstr "Modifica di massa per ricette"
|
msgstr "Modifica in blocco per ricette"
|
||||||
|
|
||||||
#: .\cookbook\templates\batch\edit.html:20
|
#: .\cookbook\templates\batch\edit.html:20
|
||||||
msgid "Add the specified keywords to all recipes containing a word"
|
msgid "Add the specified keywords to all recipes containing a word"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Aggiungi a tutte le ricette che contengono una determinata stringa le parole "
|
"Aggiungi le parole chiave che desideri a tutte le ricette che contengono una"
|
||||||
"chiave desiderate "
|
" determinata stringa"
|
||||||
|
|
||||||
#: .\cookbook\templates\batch\monitor.html:6 .\cookbook\views\edit.py:66
|
#: .\cookbook\templates\batch\monitor.html:6 .\cookbook\views\edit.py:66
|
||||||
msgid "Sync"
|
msgid "Sync"
|
||||||
@ -561,7 +556,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: .\cookbook\templates\batch\monitor.html:16
|
#: .\cookbook\templates\batch\monitor.html:16
|
||||||
msgid "The path must be in the following format"
|
msgid "The path must be in the following format"
|
||||||
msgstr "Il path deve essere nel formato seguente"
|
msgstr "Il percorso deve essere nel formato seguente"
|
||||||
|
|
||||||
#: .\cookbook\templates\batch\monitor.html:27
|
#: .\cookbook\templates\batch\monitor.html:27
|
||||||
msgid "Sync Now!"
|
msgid "Sync Now!"
|
||||||
@ -641,23 +636,19 @@ msgstr "Modifica Ricetta"
|
|||||||
|
|
||||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:62
|
#: .\cookbook\templates\forms\edit_internal_recipe.html:62
|
||||||
msgid "Waiting Time"
|
msgid "Waiting Time"
|
||||||
msgstr "Tempo di Attesa"
|
msgstr "Tempo di cottura"
|
||||||
|
|
||||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:68
|
#: .\cookbook\templates\forms\edit_internal_recipe.html:68
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Servings"
|
|
||||||
msgid "Servings Text"
|
msgid "Servings Text"
|
||||||
msgstr "Porzioni"
|
msgstr "Nome delle porzioni"
|
||||||
|
|
||||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:79
|
#: .\cookbook\templates\forms\edit_internal_recipe.html:79
|
||||||
msgid "Select Keywords"
|
msgid "Select Keywords"
|
||||||
msgstr "Seleziona parole chiave"
|
msgstr "Seleziona parole chiave"
|
||||||
|
|
||||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:93
|
#: .\cookbook\templates\forms\edit_internal_recipe.html:93
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Nutrition"
|
|
||||||
msgid "Description"
|
msgid "Description"
|
||||||
msgstr "Nutrienti"
|
msgstr "Descrizione"
|
||||||
|
|
||||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:108
|
#: .\cookbook\templates\forms\edit_internal_recipe.html:108
|
||||||
msgid "Nutrition"
|
msgid "Nutrition"
|
||||||
@ -773,7 +764,7 @@ msgstr "Abilita Quantità"
|
|||||||
|
|
||||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:348
|
#: .\cookbook\templates\forms\edit_internal_recipe.html:348
|
||||||
msgid "Copy Template Reference"
|
msgid "Copy Template Reference"
|
||||||
msgstr ""
|
msgstr "Copia riferimento template"
|
||||||
|
|
||||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:374
|
#: .\cookbook\templates\forms\edit_internal_recipe.html:374
|
||||||
#: .\cookbook\templates\url_import.html:177
|
#: .\cookbook\templates\url_import.html:177
|
||||||
@ -821,19 +812,14 @@ msgstr "Modifica Ingredienti"
|
|||||||
#: .\cookbook\templates\forms\ingredients.html:16
|
#: .\cookbook\templates\forms\ingredients.html:16
|
||||||
msgid ""
|
msgid ""
|
||||||
"\n"
|
"\n"
|
||||||
" The following form can be used if, accidentally, two (or more) units "
|
" The following form can be used if, accidentally, two (or more) units or ingredients where created that should be\n"
|
||||||
"or ingredients where created that should be\n"
|
|
||||||
" the same.\n"
|
" the same.\n"
|
||||||
" It merges two units or ingredients and updates all recipes using "
|
" It merges two units or ingredients and updates all recipes using them.\n"
|
||||||
"them.\n"
|
|
||||||
" "
|
" "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
" Questo modulo può essere utilizzato se, accidentalmente, sono stati "
|
" Questo modulo può essere utilizzato se, accidentalmente, sono stati creati due (o più) unità di misura o ingredienti che dovrebbero essere lo stesso. \n"
|
||||||
"creati due (o più) unità di misura o ingredienti che dovrebbero essere lo "
|
"Unisce due unità di misura o ingredienti e aggiorna tutte le ricette che li utilizzano."
|
||||||
"stesso. \n"
|
|
||||||
"Unisce due unità di misura o ingredienti e aggiorna tutte le ricette che li "
|
|
||||||
"utilizzano."
|
|
||||||
|
|
||||||
#: .\cookbook\templates\forms\ingredients.html:24
|
#: .\cookbook\templates\forms\ingredients.html:24
|
||||||
#: .\cookbook\templates\stats.html:26
|
#: .\cookbook\templates\stats.html:26
|
||||||
@ -955,19 +941,15 @@ msgstr "Avviso di Sicurezza"
|
|||||||
#: .\cookbook\templates\include\storage_backend_warning.html:5
|
#: .\cookbook\templates\include\storage_backend_warning.html:5
|
||||||
msgid ""
|
msgid ""
|
||||||
"\n"
|
"\n"
|
||||||
" The <b>Password and Token</b> field are stored as <b>plain text</b> "
|
" The <b>Password and Token</b> field are stored as <b>plain text</b> inside the database.\n"
|
||||||
"inside the database.\n"
|
" This is necessary because they are needed to make API requests, but it also increases the risk of\n"
|
||||||
" This is necessary because they are needed to make API requests, but "
|
|
||||||
"it also increases the risk of\n"
|
|
||||||
" someone stealing it. <br/>\n"
|
" someone stealing it. <br/>\n"
|
||||||
" To limit the possible damage tokens or accounts with limited access "
|
" To limit the possible damage tokens or accounts with limited access can be used.\n"
|
||||||
"can be used.\n"
|
|
||||||
" "
|
" "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
"I campi <b>Password e Token</b> sono salvati <b>in chiaro</b> nel database.\n"
|
"I campi <b>Password e Token</b> sono salvati <b>in chiaro</b> nel database.\n"
|
||||||
"È necessario perché servono per fare richieste API, ma questo aumenta il "
|
"È necessario perché servono per fare richieste API, ma questo aumenta il rischio che\n"
|
||||||
"rischio che\n"
|
|
||||||
"qualcuno possa impossessarsene.<br/>\n"
|
"qualcuno possa impossessarsene.<br/>\n"
|
||||||
"Per liminare il danno puoi usare account con accesso limitato o i token."
|
"Per liminare il danno puoi usare account con accesso limitato o i token."
|
||||||
|
|
||||||
@ -1012,29 +994,19 @@ msgstr "Informazioni su Markdown"
|
|||||||
#: .\cookbook\templates\markdown_info.html:14
|
#: .\cookbook\templates\markdown_info.html:14
|
||||||
msgid ""
|
msgid ""
|
||||||
"\n"
|
"\n"
|
||||||
" Markdown is lightweight markup language that can be used to format "
|
" Markdown is lightweight markup language that can be used to format plain text easily.\n"
|
||||||
"plain text easily.\n"
|
" This site uses the <a href=\"https://python-markdown.github.io/\" target=\"_blank\">Python Markdown</a> library to\n"
|
||||||
" This site uses the <a href=\"https://python-markdown.github.io/\" "
|
" convert your text into nice looking HTML. Its full markdown documentation can be found\n"
|
||||||
"target=\"_blank\">Python Markdown</a> library to\n"
|
" <a href=\"https://daringfireball.net/projects/markdown/syntax\" target=\"_blank\">here</a>.\n"
|
||||||
" convert your text into nice looking HTML. Its full markdown "
|
" An incomplete but most likely sufficient documentation can be found below.\n"
|
||||||
"documentation can be found\n"
|
|
||||||
" <a href=\"https://daringfireball.net/projects/markdown/syntax\" "
|
|
||||||
"target=\"_blank\">here</a>.\n"
|
|
||||||
" An incomplete but most likely sufficient documentation can be found "
|
|
||||||
"below.\n"
|
|
||||||
" "
|
" "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
" Markdown è un linguaggio di markup molto leggero che può essere "
|
" Markdown è un linguaggio di markup molto leggero che può essere utilizzato per formattare facilmente del testo.\n"
|
||||||
"utilizzato per formattare facilmente del testo.\n"
|
" Questo sito utilizza la libreria <a href=\"https://python-markdown.github.io/\" target=\"_blank\">Python Markdown</a> per\n"
|
||||||
" Questo sito utilizza la libreria <a href=\"https://python-markdown."
|
" convertire il tuo testo in HTML formattato. È possibile trovare la documentazione completa del markdown\n"
|
||||||
"github.io/\" target=\"_blank\">Python Markdown</a> per\n"
|
" <a href=\"https://daringfireball.net/projects/markdown/syntax\" target=\"_blank\">qui</a>.\n"
|
||||||
" convertire il tuo testo in HTML formattato. È possibile trovare la "
|
" Di seguito è possibile trovare una documentazione incompleta ma molto probabilmente sufficiente."
|
||||||
"documentazione completa del markdown\n"
|
|
||||||
" <a href=\"https://daringfireball.net/projects/markdown/syntax\" target="
|
|
||||||
"\"_blank\">qui</a>.\n"
|
|
||||||
" Di seguito è possibile trovare una documentazione incompleta ma molto "
|
|
||||||
"probabilmente sufficiente."
|
|
||||||
|
|
||||||
#: .\cookbook\templates\markdown_info.html:25
|
#: .\cookbook\templates\markdown_info.html:25
|
||||||
msgid "Headers"
|
msgid "Headers"
|
||||||
@ -1134,19 +1106,15 @@ msgid "Tables"
|
|||||||
msgstr "Tabelle"
|
msgstr "Tabelle"
|
||||||
|
|
||||||
#: .\cookbook\templates\markdown_info.html:153
|
#: .\cookbook\templates\markdown_info.html:153
|
||||||
#, fuzzy
|
|
||||||
#| msgid ""
|
|
||||||
#| "Markdown tables are hard to create by hand. It is recommended to use a "
|
|
||||||
#| "table editor like <a href=\"https://www.tablesgenerator.com/"
|
|
||||||
#| "markdown_tables\" target=\"_blank\">this</a> one."
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"Markdown tables are hard to create by hand. It is recommended to use a table"
|
"Markdown tables are hard to create by hand. It is recommended to use a table"
|
||||||
"editor like <a href=\"https://www.tablesgenerator.com/markdown_tables\" rel="
|
" editor like <a href=\"https://www.tablesgenerator.com/markdown_tables\" "
|
||||||
"\"noreferrer noopener\" target=\"_blank\">this one.</a>"
|
"rel=\"noreferrer noopener\" target=\"_blank\">this one.</a>"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Le tabelle in markdown sono difficili da creare a mano. È raccomandato "
|
"Le tabelle in markdown sono difficili da creare a mano. Si raccomanda "
|
||||||
"utilizzare un editor di tabelle come <a href=\"https://www.tablesgenerator."
|
"l'utilizzo di un editor di come <a "
|
||||||
"com/markdown_tables\" target=\"_blank\">questo</a>."
|
"href=\"https://www.tablesgenerator.com/markdown_tables\" "
|
||||||
|
"target=\"_blank\">questo</a>."
|
||||||
|
|
||||||
#: .\cookbook\templates\markdown_info.html:155
|
#: .\cookbook\templates\markdown_info.html:155
|
||||||
#: .\cookbook\templates\markdown_info.html:157
|
#: .\cookbook\templates\markdown_info.html:157
|
||||||
@ -1180,23 +1148,22 @@ msgstr "Titolo"
|
|||||||
|
|
||||||
#: .\cookbook\templates\meal_plan.html:141
|
#: .\cookbook\templates\meal_plan.html:141
|
||||||
msgid "Note (optional)"
|
msgid "Note (optional)"
|
||||||
msgstr "Note (opzionale)"
|
msgstr "Nota (opzionale)"
|
||||||
|
|
||||||
#: .\cookbook\templates\meal_plan.html:143
|
#: .\cookbook\templates\meal_plan.html:143
|
||||||
msgid ""
|
msgid ""
|
||||||
"You can use markdown to format this field. See the <a href=\"/docs/markdown/"
|
"You can use markdown to format this field. See the <a "
|
||||||
"\" target=\"_blank\" rel=\"noopener noreferrer\">docs here</a>"
|
"href=\"/docs/markdown/\" target=\"_blank\" rel=\"noopener noreferrer\">docs "
|
||||||
|
"here</a>"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Puoi usare markdown per formattare questo campo. Guarda la <a href=\"/docs/"
|
"Puoi usare markdown per formattare questo campo. Guarda la <a "
|
||||||
"markdown/\" target=\"_blank\" rel=\"noopener noreferrer\">documentazione "
|
"href=\"/docs/markdown/\" target=\"_blank\" rel=\"noopener "
|
||||||
"qui</a>"
|
"noreferrer\">documentazione qui</a>"
|
||||||
|
|
||||||
#: .\cookbook\templates\meal_plan.html:147
|
#: .\cookbook\templates\meal_plan.html:147
|
||||||
#: .\cookbook\templates\meal_plan.html:251
|
#: .\cookbook\templates\meal_plan.html:251
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Servings"
|
|
||||||
msgid "Serving Count"
|
msgid "Serving Count"
|
||||||
msgstr "Porzioni"
|
msgstr "Numero di porzioni"
|
||||||
|
|
||||||
#: .\cookbook\templates\meal_plan.html:153
|
#: .\cookbook\templates\meal_plan.html:153
|
||||||
msgid "Create only note"
|
msgid "Create only note"
|
||||||
@ -1274,98 +1241,40 @@ msgid "Meal Plan Help"
|
|||||||
msgstr "Aiuto per il piano alimentare"
|
msgstr "Aiuto per il piano alimentare"
|
||||||
|
|
||||||
#: .\cookbook\templates\meal_plan.html:344
|
#: .\cookbook\templates\meal_plan.html:344
|
||||||
#, fuzzy
|
|
||||||
#| msgid ""
|
|
||||||
#| "\n"
|
|
||||||
#| " <p>The meal plan module allows planning of "
|
|
||||||
#| "meals both with recipes or just notes.</p>\n"
|
|
||||||
#| " <p>Simply select a recipe from the list of "
|
|
||||||
#| "recently viewed recipes or search the one you\n"
|
|
||||||
#| " want and drag it to the desired plan "
|
|
||||||
#| "position. You can also add a note and a title and\n"
|
|
||||||
#| " then drag the recipe to create a plan "
|
|
||||||
#| "entry with a custom title and note. Creating only\n"
|
|
||||||
#| " Notes is possible by dragging the create "
|
|
||||||
#| "note box into the plan.</p>\n"
|
|
||||||
#| " <p>Click on a recipe in order to open the "
|
|
||||||
#| "detail view. Here you can also add it to the\n"
|
|
||||||
#| " shopping list. You can also add all "
|
|
||||||
#| "recipes of a day to the shopping list by\n"
|
|
||||||
#| " clicking the shopping cart at the top of "
|
|
||||||
#| "the table.</p>\n"
|
|
||||||
#| " <p>Since a common use case is to plan meals "
|
|
||||||
#| "together you can define\n"
|
|
||||||
#| " users you want to share your plan with in "
|
|
||||||
#| "the settings.\n"
|
|
||||||
#| " </p>\n"
|
|
||||||
#| " <p>You can also edit the types of meals you "
|
|
||||||
#| "want to plan. If you share your plan with\n"
|
|
||||||
#| " someone with\n"
|
|
||||||
#| " different meals, their meal types will "
|
|
||||||
#| "appear in your list as well. To prevent\n"
|
|
||||||
#| " duplicates (e.g. Other and Misc.)\n"
|
|
||||||
#| " name your meal types the same as the "
|
|
||||||
#| "users you share your meals with and they will be\n"
|
|
||||||
#| " merged.</p>\n"
|
|
||||||
#| " "
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"\n"
|
"\n"
|
||||||
" <p>The meal plan module allows planning of meals "
|
" <p>The meal plan module allows planning of meals both with recipes and notes.</p>\n"
|
||||||
"both with recipes and notes.</p>\n"
|
" <p>Simply select a recipe from the list of recently viewed recipes or search the one you\n"
|
||||||
" <p>Simply select a recipe from the list of "
|
" want and drag it to the desired plan position. You can also add a note and a title and\n"
|
||||||
"recently viewed recipes or search the one you\n"
|
" then drag the recipe to create a plan entry with a custom title and note. Creating only\n"
|
||||||
" want and drag it to the desired plan "
|
" Notes is possible by dragging the create note box into the plan.</p>\n"
|
||||||
"position. You can also add a note and a title and\n"
|
" <p>Click on a recipe in order to open the detailed view. There you can also add it to the\n"
|
||||||
" then drag the recipe to create a plan entry "
|
" shopping list. You can also add all recipes of a day to the shopping list by\n"
|
||||||
"with a custom title and note. Creating only\n"
|
" clicking the shopping cart at the top of the table.</p>\n"
|
||||||
" Notes is possible by dragging the create "
|
" <p>Since a common use case is to plan meals together you can define\n"
|
||||||
"note box into the plan.</p>\n"
|
" users you want to share your plan with in the settings.\n"
|
||||||
" <p>Click on a recipe in order to open the "
|
|
||||||
"detailed view. There you can also add it to the\n"
|
|
||||||
" shopping list. You can also add all recipes "
|
|
||||||
"of a day to the shopping list by\n"
|
|
||||||
" clicking the shopping cart at the top of the "
|
|
||||||
"table.</p>\n"
|
|
||||||
" <p>Since a common use case is to plan meals "
|
|
||||||
"together you can define\n"
|
|
||||||
" users you want to share your plan with in "
|
|
||||||
"the settings.\n"
|
|
||||||
" </p>\n"
|
" </p>\n"
|
||||||
" <p>You can also edit the types of meals you want "
|
" <p>You can also edit the types of meals you want to plan. If you share your plan with\n"
|
||||||
"to plan. If you share your plan with\n"
|
|
||||||
" someone with\n"
|
" someone with\n"
|
||||||
" different meals, their meal types will "
|
" different meals, their meal types will appear in your list as well. To prevent\n"
|
||||||
"appear in your list as well. To prevent\n"
|
|
||||||
" duplicates (e.g. Other and Misc.)\n"
|
" duplicates (e.g. Other and Misc.)\n"
|
||||||
" name your meal types the same as the users "
|
" name your meal types the same as the users you share your meals with and they will be\n"
|
||||||
"you share your meals with and they will be\n"
|
|
||||||
" merged.</p>\n"
|
" merged.</p>\n"
|
||||||
" "
|
" "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
"<p>Il modulo del piano alimentare consente di pianificare i pasti sia con "
|
"<p>Il modulo del piano alimentare consente di pianificare i pasti sia con ricette che con semplici note.</p>\n"
|
||||||
"ricette che con semplici note.</p>\n"
|
|
||||||
"<p>Seleziona una ricetta dalla lista delle ricette recenti o cercane una,\n"
|
"<p>Seleziona una ricetta dalla lista delle ricette recenti o cercane una,\n"
|
||||||
"quindi spostala sulla posizione desiderata. Puoi anche aggiungere una nota e "
|
"quindi spostala sulla posizione desiderata. Puoi anche aggiungere una nota e un titolo e\n"
|
||||||
"un titolo e\n"
|
"poi trascinare la ricetta per creare una voce nel piano con un titolo e una nota personalizzata. Si possono anche creare\n"
|
||||||
"poi trascinare la ricetta per creare una voce nel piano con un titolo e una "
|
|
||||||
"nota personalizzata. Si possono anche creare\n"
|
|
||||||
"delle note trascinando la casella della nota nel piano.</p>\n"
|
"delle note trascinando la casella della nota nel piano.</p>\n"
|
||||||
"<p>Clicca su una ricetta per aprire la pagina dei dettagli. Qui potrai anche "
|
"<p>Clicca su una ricetta per aprire la vista dettagliata. Qui potrai anche aggiungerla alla lista della spesa. Puoi anche aggiungere tutte le ricette di un giorno alla lista della spesa, basterà cliccare sul carrello sopra la tabella.</p>\n"
|
||||||
"aggiungerla alla lista della spesa. Puoi anche aggiungere tutte le ricette "
|
"<p>Dato che è comune pianificare i pasti con altre persone, nelle impostazioni puoi scegliere gli utenti con i quali condividere il tuo piano.</p>\n"
|
||||||
"di un giorno alla lista della spesa, basterà cliccare sul carrello sopra la "
|
"<p>Puoi anche modificare i tipi di pasto che vuoi pianificare. Se condividi il piano con\n"
|
||||||
"tabella.</p>\n"
|
|
||||||
"<p>Dato che è comune pianificare i pasti con qualcun altro, nelle "
|
|
||||||
"impostazioni puoi scegliere gli utenti con i quali condividere il tuo piano."
|
|
||||||
"</p>\n"
|
|
||||||
"<p>Puoi anche modificare i tipi di pasto che vuoi pianificare. Se condividi "
|
|
||||||
"il piano con\n"
|
|
||||||
"qualcuno\n"
|
"qualcuno\n"
|
||||||
"con pasti differenti, i loro tipi di pasto appariranno anche nella tua "
|
"con pasti differenti, i loro tipi di pasto appariranno anche nella tua lista. Per evitare\n"
|
||||||
"lista. Per prevenire\n"
|
|
||||||
"duplicati (es. Altri e Varie)\n"
|
"duplicati (es. Altri e Varie)\n"
|
||||||
"dai nomi ai tuoi tipi di pasto uguali ai tuoi utenti in modo che verranno "
|
"dai nomi ai tuoi tipi di pasto uguali ai tuoi utenti in modo che verranno uniti.</p>"
|
||||||
"uniti.</p>"
|
|
||||||
|
|
||||||
#: .\cookbook\templates\meal_plan_entry.html:6
|
#: .\cookbook\templates\meal_plan_entry.html:6
|
||||||
msgid "Meal Plan View"
|
msgid "Meal Plan View"
|
||||||
@ -1382,27 +1291,32 @@ msgstr "Altri pasti di questo giorno"
|
|||||||
#: .\cookbook\templates\no_groups_info.html:5
|
#: .\cookbook\templates\no_groups_info.html:5
|
||||||
#: .\cookbook\templates\offline.html:6
|
#: .\cookbook\templates\offline.html:6
|
||||||
msgid "Offline"
|
msgid "Offline"
|
||||||
msgstr ""
|
msgstr "Non in linea"
|
||||||
|
|
||||||
#: .\cookbook\templates\no_groups_info.html:12
|
#: .\cookbook\templates\no_groups_info.html:12
|
||||||
msgid "No Permissions"
|
msgid "No Permissions"
|
||||||
msgstr ""
|
msgstr "Nessun permesso"
|
||||||
|
|
||||||
#: .\cookbook\templates\no_groups_info.html:15
|
#: .\cookbook\templates\no_groups_info.html:15
|
||||||
msgid ""
|
msgid ""
|
||||||
"You do not have any groups and therefor cannot use this application. Please "
|
"You do not have any groups and therefor cannot use this application. Please "
|
||||||
"contact your administrator."
|
"contact your administrator."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Non fai parte di un gruppo e questo non ti consente di usare l'applicazione."
|
||||||
|
" Contatta il tuo amministratore."
|
||||||
|
|
||||||
#: .\cookbook\templates\offline.html:19
|
#: .\cookbook\templates\offline.html:19
|
||||||
msgid "You are currently offline!"
|
msgid "You are currently offline!"
|
||||||
msgstr ""
|
msgstr "Al momento sei offline!"
|
||||||
|
|
||||||
#: .\cookbook\templates\offline.html:20
|
#: .\cookbook\templates\offline.html:20
|
||||||
msgid ""
|
msgid ""
|
||||||
"The recipes listed below are available for offline viewing because you have "
|
"The recipes listed below are available for offline viewing because you have "
|
||||||
"recently viewed them. Keep in mind that data might be outdated."
|
"recently viewed them. Keep in mind that data might be outdated."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Le ricette qui sotto sono disponibili per essere consultate quando sei "
|
||||||
|
"offline perché le hai aperte di recente. Ricorda che queste informazioni "
|
||||||
|
"potrebbero non essere aggiornate."
|
||||||
|
|
||||||
#: .\cookbook\templates\recipe_view.html:21 .\cookbook\templates\stats.html:47
|
#: .\cookbook\templates\recipe_view.html:21 .\cookbook\templates\stats.html:47
|
||||||
msgid "Comments"
|
msgid "Comments"
|
||||||
@ -1427,7 +1341,7 @@ msgstr "Tempo di preparazione circa"
|
|||||||
#: .\cookbook\templates\recipes_table.html:52
|
#: .\cookbook\templates\recipes_table.html:52
|
||||||
#: .\cookbook\templates\url_import.html:60
|
#: .\cookbook\templates\url_import.html:60
|
||||||
msgid "Waiting time ca."
|
msgid "Waiting time ca."
|
||||||
msgstr "Tempo di attesa circa"
|
msgstr "Tempo di cottura circa"
|
||||||
|
|
||||||
#: .\cookbook\templates\recipes_table.html:55
|
#: .\cookbook\templates\recipes_table.html:55
|
||||||
msgid "External"
|
msgid "External"
|
||||||
@ -1447,7 +1361,7 @@ msgstr "Account"
|
|||||||
|
|
||||||
#: .\cookbook\templates\settings.html:38
|
#: .\cookbook\templates\settings.html:38
|
||||||
msgid "Link social account"
|
msgid "Link social account"
|
||||||
msgstr ""
|
msgstr "Collega account social"
|
||||||
|
|
||||||
#: .\cookbook\templates\settings.html:42
|
#: .\cookbook\templates\settings.html:42
|
||||||
msgid "Language"
|
msgid "Language"
|
||||||
@ -1509,13 +1423,11 @@ msgstr "Nessuna ricetta selezionata"
|
|||||||
|
|
||||||
#: .\cookbook\templates\shopping_list.html:145
|
#: .\cookbook\templates\shopping_list.html:145
|
||||||
msgid "Entry Mode"
|
msgid "Entry Mode"
|
||||||
msgstr ""
|
msgstr "Modalità di inserimento"
|
||||||
|
|
||||||
#: .\cookbook\templates\shopping_list.html:153
|
#: .\cookbook\templates\shopping_list.html:153
|
||||||
#, fuzzy
|
|
||||||
#| msgid "New Entry"
|
|
||||||
msgid "Add Entry"
|
msgid "Add Entry"
|
||||||
msgstr "Nuovo Campo"
|
msgstr "Aggiungi voce"
|
||||||
|
|
||||||
#: .\cookbook\templates\shopping_list.html:168
|
#: .\cookbook\templates\shopping_list.html:168
|
||||||
msgid "Amount"
|
msgid "Amount"
|
||||||
@ -1523,13 +1435,11 @@ msgstr "Quantità"
|
|||||||
|
|
||||||
#: .\cookbook\templates\shopping_list.html:224
|
#: .\cookbook\templates\shopping_list.html:224
|
||||||
msgid "Supermarket"
|
msgid "Supermarket"
|
||||||
msgstr ""
|
msgstr "Supermercato"
|
||||||
|
|
||||||
#: .\cookbook\templates\shopping_list.html:234
|
#: .\cookbook\templates\shopping_list.html:234
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Select User"
|
|
||||||
msgid "Select Supermarket"
|
msgid "Select Supermarket"
|
||||||
msgstr "Seleziona utente"
|
msgstr "Seleziona supermercato"
|
||||||
|
|
||||||
#: .\cookbook\templates\shopping_list.html:258
|
#: .\cookbook\templates\shopping_list.html:258
|
||||||
msgid "Select User"
|
msgid "Select User"
|
||||||
@ -1558,26 +1468,27 @@ msgstr "Si è verificato un errore durante la creazione di una risorsa!"
|
|||||||
#: .\cookbook\templates\socialaccount\connections.html:4
|
#: .\cookbook\templates\socialaccount\connections.html:4
|
||||||
#: .\cookbook\templates\socialaccount\connections.html:7
|
#: .\cookbook\templates\socialaccount\connections.html:7
|
||||||
msgid "Account Connections"
|
msgid "Account Connections"
|
||||||
msgstr ""
|
msgstr "Collegamenti dell'account"
|
||||||
|
|
||||||
#: .\cookbook\templates\socialaccount\connections.html:10
|
#: .\cookbook\templates\socialaccount\connections.html:10
|
||||||
msgid ""
|
msgid ""
|
||||||
"You can sign in to your account using any of the following third party\n"
|
"You can sign in to your account using any of the following third party\n"
|
||||||
" accounts:"
|
" accounts:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Puoi accedere al tuo account usando uno dei seguenti account di terze parti:"
|
||||||
|
|
||||||
#: .\cookbook\templates\socialaccount\connections.html:36
|
#: .\cookbook\templates\socialaccount\connections.html:36
|
||||||
msgid "Remove"
|
msgid "Remove"
|
||||||
msgstr ""
|
msgstr "Rimuovi"
|
||||||
|
|
||||||
#: .\cookbook\templates\socialaccount\connections.html:44
|
#: .\cookbook\templates\socialaccount\connections.html:44
|
||||||
msgid ""
|
msgid ""
|
||||||
"You currently have no social network accounts connected to this account."
|
"You currently have no social network accounts connected to this account."
|
||||||
msgstr ""
|
msgstr "Non hai account di social network collegati a questo account."
|
||||||
|
|
||||||
#: .\cookbook\templates\socialaccount\connections.html:47
|
#: .\cookbook\templates\socialaccount\connections.html:47
|
||||||
msgid "Add a 3rd Party Account"
|
msgid "Add a 3rd Party Account"
|
||||||
msgstr ""
|
msgstr "Aggiungi un account di terze parti"
|
||||||
|
|
||||||
#: .\cookbook\templates\stats.html:4
|
#: .\cookbook\templates\stats.html:4
|
||||||
msgid "Stats"
|
msgid "Stats"
|
||||||
@ -1630,18 +1541,14 @@ msgstr "Informazioni di sistema"
|
|||||||
#: .\cookbook\templates\system.html:51
|
#: .\cookbook\templates\system.html:51
|
||||||
msgid ""
|
msgid ""
|
||||||
"\n"
|
"\n"
|
||||||
" Django Recipes is an open source free software application. It can "
|
" Django Recipes is an open source free software application. It can be found on\n"
|
||||||
"be found on\n"
|
|
||||||
" <a href=\"https://github.com/vabene1111/recipes\">GitHub</a>.\n"
|
" <a href=\"https://github.com/vabene1111/recipes\">GitHub</a>.\n"
|
||||||
" Changelogs can be found <a href=\"https://github.com/vabene1111/"
|
" Changelogs can be found <a href=\"https://github.com/vabene1111/recipes/releases\">here</a>.\n"
|
||||||
"recipes/releases\">here</a>.\n"
|
|
||||||
" "
|
" "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
"Django Recipes è una applicazione gratuita e open source. È disponibile su "
|
"Django Recipes è una applicazione gratuita e open source. È disponibile su <a href=\"https://github.com/vabene1111/recipes\">GitHub</a>.\n"
|
||||||
"<a href=\"https://github.com/vabene1111/recipes\">GitHub</a>.\n"
|
"Le ultime novità sono disponibili <a href=\"https://github.com/vabene1111/recipes/releases\">qui</a>."
|
||||||
"Le ultime novità sono disponibili <a href=\"https://github.com/vabene1111/"
|
|
||||||
"recipes/releases\">qui</a>."
|
|
||||||
|
|
||||||
#: .\cookbook\templates\system.html:65
|
#: .\cookbook\templates\system.html:65
|
||||||
msgid "Media Serving"
|
msgid "Media Serving"
|
||||||
@ -1661,16 +1568,13 @@ msgstr "Ok"
|
|||||||
msgid ""
|
msgid ""
|
||||||
"Serving media files directly using gunicorn/python is <b>not recommend</b>!\n"
|
"Serving media files directly using gunicorn/python is <b>not recommend</b>!\n"
|
||||||
" Please follow the steps described\n"
|
" Please follow the steps described\n"
|
||||||
" <a href=\"https://github.com/vabene1111/recipes/releases/"
|
" <a href=\"https://github.com/vabene1111/recipes/releases/tag/0.8.1\">here</a> to update\n"
|
||||||
"tag/0.8.1\">here</a> to update\n"
|
|
||||||
" your installation.\n"
|
" your installation.\n"
|
||||||
" "
|
" "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Erogare i file multimediali usando gunicorn/python <b>non è raccomandato</"
|
"Erogare i file multimediali usando gunicorn/python <b>non è raccomandato</b>!\n"
|
||||||
"b>!\n"
|
|
||||||
"Segui i passi descritti\n"
|
"Segui i passi descritti\n"
|
||||||
"<a href=\"https://github.com/vabene1111/recipes/releases/tag/0.8.1\">qui</a> "
|
"<a href=\"https://github.com/vabene1111/recipes/releases/tag/0.8.1\">qui</a> per aggiornare la tua installazione."
|
||||||
"per aggiornare la tua installazione."
|
|
||||||
|
|
||||||
#: .\cookbook\templates\system.html:74 .\cookbook\templates\system.html:90
|
#: .\cookbook\templates\system.html:74 .\cookbook\templates\system.html:90
|
||||||
#: .\cookbook\templates\system.html:105 .\cookbook\templates\system.html:119
|
#: .\cookbook\templates\system.html:105 .\cookbook\templates\system.html:119
|
||||||
@ -1684,18 +1588,14 @@ msgstr "Chiave segreta"
|
|||||||
#: .\cookbook\templates\system.html:83
|
#: .\cookbook\templates\system.html:83
|
||||||
msgid ""
|
msgid ""
|
||||||
"\n"
|
"\n"
|
||||||
" You do not have a <code>SECRET_KEY</code> configured in your "
|
" You do not have a <code>SECRET_KEY</code> configured in your <code>.env</code> file. Django defaulted to the\n"
|
||||||
"<code>.env</code> file. Django defaulted to the\n"
|
|
||||||
" standard key\n"
|
" standard key\n"
|
||||||
" provided with the installation which is publicly know and "
|
" provided with the installation which is publicly know and insecure! Please set\n"
|
||||||
"insecure! Please set\n"
|
" <code>SECRET_KEY</code> int the <code>.env</code> configuration file.\n"
|
||||||
" <code>SECRET_KEY</code> int the <code>.env</code> configuration "
|
|
||||||
"file.\n"
|
|
||||||
" "
|
" "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
"Non hai inserito una <code>SECRET_KEY</code> nel file <code>.env</code>. "
|
"Non hai inserito una <code>SECRET_KEY</code> nel file <code>.env</code>. Django ha dovuto usare la chiave standard\n"
|
||||||
"Django ha dovuto usare la chiave standard\n"
|
|
||||||
"dell'installazione che è pubblica e insicura! Sei pregato di aggiungere una\n"
|
"dell'installazione che è pubblica e insicura! Sei pregato di aggiungere una\n"
|
||||||
"<code>SECRET_KEY</code> nel file di configurazione <code>.env</code>."
|
"<code>SECRET_KEY</code> nel file di configurazione <code>.env</code>."
|
||||||
|
|
||||||
@ -1706,16 +1606,13 @@ msgstr "Modalità di debug"
|
|||||||
#: .\cookbook\templates\system.html:99
|
#: .\cookbook\templates\system.html:99
|
||||||
msgid ""
|
msgid ""
|
||||||
"\n"
|
"\n"
|
||||||
" This application is still running in debug mode. This is most "
|
" This application is still running in debug mode. This is most likely not needed. Turn of debug mode by\n"
|
||||||
"likely not needed. Turn of debug mode by\n"
|
|
||||||
" setting\n"
|
" setting\n"
|
||||||
" <code>DEBUG=0</code> int the <code>.env</code> configuration "
|
" <code>DEBUG=0</code> int the <code>.env</code> configuration file.\n"
|
||||||
"file.\n"
|
|
||||||
" "
|
" "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
"Questa applicazione è in esecuzione in modalità di debug. Probabilmente non "
|
"Questa applicazione è in esecuzione in modalità di debug. Probabilmente non è necessario, spegni la modalità di debug \n"
|
||||||
"è necessario, spegni la modalità di debug \n"
|
|
||||||
"configurando\n"
|
"configurando\n"
|
||||||
"<code>DEBUG=0</code> nel file di configurazione<code>.env</code>."
|
"<code>DEBUG=0</code> nel file di configurazione<code>.env</code>."
|
||||||
|
|
||||||
@ -1730,14 +1627,12 @@ msgstr "Info"
|
|||||||
#: .\cookbook\templates\system.html:114
|
#: .\cookbook\templates\system.html:114
|
||||||
msgid ""
|
msgid ""
|
||||||
"\n"
|
"\n"
|
||||||
" This application is not running with a Postgres database "
|
" This application is not running with a Postgres database backend. This is ok but not recommended as some\n"
|
||||||
"backend. This is ok but not recommended as some\n"
|
|
||||||
" features only work with postgres databases.\n"
|
" features only work with postgres databases.\n"
|
||||||
" "
|
" "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
"Questa applicazione non sta girando su un database Postgres. Non è "
|
"Questa applicazione non sta girando su un database Postgres. Non è raccomandato perché alcune\n"
|
||||||
"raccomandato perché alcune\n"
|
|
||||||
"funzionalità sono disponibili solo con un database Posgres."
|
"funzionalità sono disponibili solo con un database Posgres."
|
||||||
|
|
||||||
#: .\cookbook\templates\url_import.html:5
|
#: .\cookbook\templates\url_import.html:5
|
||||||
@ -1773,18 +1668,14 @@ msgstr "Info"
|
|||||||
#: .\cookbook\templates\url_import.html:235
|
#: .\cookbook\templates\url_import.html:235
|
||||||
msgid ""
|
msgid ""
|
||||||
" Only websites containing ld+json or microdata information can currently\n"
|
" Only websites containing ld+json or microdata information can currently\n"
|
||||||
" be imported. Most big recipe pages "
|
" be imported. Most big recipe pages support this. If you site cannot be imported but\n"
|
||||||
"support this. If you site cannot be imported but\n"
|
|
||||||
" you think\n"
|
" you think\n"
|
||||||
" it probably has some kind of structured "
|
" it probably has some kind of structured data feel free to post an example in the\n"
|
||||||
"data feel free to post an example in the\n"
|
|
||||||
" github issues."
|
" github issues."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Possono essere importati solo i siti che contengono informazioni Id+json o "
|
"Possono essere importati solo i siti che contengono informazioni Id+json o microdata.\n"
|
||||||
"microdata.\n"
|
|
||||||
"I maggiori siti di ricette di solito sono supportati.\n"
|
"I maggiori siti di ricette di solito sono supportati.\n"
|
||||||
"Se questo sito non può essere importato ma credi che abbia una qualche tipo "
|
"Se questo sito non può essere importato ma credi che abbia una qualche tipo di struttura dati, puoi inviare un esempio nella sezione Issues su GitHub."
|
||||||
"di struttura dati, puoi inviare un esempio nella sezione Issues su GitHub."
|
|
||||||
|
|
||||||
#: .\cookbook\templates\url_import.html:243
|
#: .\cookbook\templates\url_import.html:243
|
||||||
msgid "Google ld+json Info"
|
msgid "Google ld+json Info"
|
||||||
@ -1808,7 +1699,7 @@ msgstr "La preferenza per l'utente fornito esiste già"
|
|||||||
|
|
||||||
#: .\cookbook\views\api.py:416 .\cookbook\views\views.py:265
|
#: .\cookbook\views\api.py:416 .\cookbook\views\views.py:265
|
||||||
msgid "This feature is not available in the demo version!"
|
msgid "This feature is not available in the demo version!"
|
||||||
msgstr ""
|
msgstr "Questa funzione non è disponibile nella versione demo!"
|
||||||
|
|
||||||
#: .\cookbook\views\api.py:439
|
#: .\cookbook\views\api.py:439
|
||||||
msgid "Sync successful!"
|
msgid "Sync successful!"
|
||||||
@ -1832,9 +1723,10 @@ msgstr ""
|
|||||||
#, python-format
|
#, python-format
|
||||||
msgid "Batch edit done. %(count)d recipe was updated."
|
msgid "Batch edit done. %(count)d recipe was updated."
|
||||||
msgid_plural "Batch edit done. %(count)d Recipes where updated."
|
msgid_plural "Batch edit done. %(count)d Recipes where updated."
|
||||||
msgstr[0] "Modifica di massa completata. %(count)d ricetta è stata aggiornata."
|
msgstr[0] ""
|
||||||
|
"Modifica di massa completata. %(count)d ricetta è stata aggiornata."
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
"Modifica di massa completata. %(count)d ricette sono state aggiornate."
|
"Modifica in blocco completata. %(count)d ricette sono state aggiornate."
|
||||||
|
|
||||||
#: .\cookbook\views\delete.py:72
|
#: .\cookbook\views\delete.py:72
|
||||||
msgid "Monitor"
|
msgid "Monitor"
|
||||||
@ -1901,7 +1793,7 @@ msgstr "Le unità sono state unite!"
|
|||||||
|
|
||||||
#: .\cookbook\views\edit.py:295 .\cookbook\views\edit.py:317
|
#: .\cookbook\views\edit.py:295 .\cookbook\views\edit.py:317
|
||||||
msgid "Cannot merge with the same object!"
|
msgid "Cannot merge with the same object!"
|
||||||
msgstr ""
|
msgstr "Non è possibile unirlo con lo stesso oggetto!"
|
||||||
|
|
||||||
#: .\cookbook\views\edit.py:311
|
#: .\cookbook\views\edit.py:311
|
||||||
msgid "Foods merged!"
|
msgid "Foods merged!"
|
||||||
@ -1909,11 +1801,11 @@ msgstr "Gli alimenti sono stati uniti!"
|
|||||||
|
|
||||||
#: .\cookbook\views\import_export.py:42
|
#: .\cookbook\views\import_export.py:42
|
||||||
msgid "Importing is not implemented for this provider"
|
msgid "Importing is not implemented for this provider"
|
||||||
msgstr ""
|
msgstr "Questo provider non permette l'importazione"
|
||||||
|
|
||||||
#: .\cookbook\views\import_export.py:58
|
#: .\cookbook\views\import_export.py:58
|
||||||
msgid "Exporting is not implemented for this provider"
|
msgid "Exporting is not implemented for this provider"
|
||||||
msgstr ""
|
msgstr "Questo provider non permette l'esportazione"
|
||||||
|
|
||||||
#: .\cookbook\views\lists.py:42
|
#: .\cookbook\views\lists.py:42
|
||||||
msgid "Import Log"
|
msgid "Import Log"
|
||||||
@ -1937,7 +1829,7 @@ msgstr "Si è verificato un errore durante l'importazione di questa ricetta!"
|
|||||||
|
|
||||||
#: .\cookbook\views\views.py:117
|
#: .\cookbook\views\views.py:117
|
||||||
msgid "You do not have the required permissions to perform this action!"
|
msgid "You do not have the required permissions to perform this action!"
|
||||||
msgstr "Non hai i permessi necessari per effettuare questa operazione!"
|
msgstr "Non hai i permessi necessari per completare questa operazione!"
|
||||||
|
|
||||||
#: .\cookbook\views\views.py:136
|
#: .\cookbook\views\views.py:136
|
||||||
msgid "Comment saved!"
|
msgid "Comment saved!"
|
||||||
@ -1945,7 +1837,7 @@ msgstr "Commento salvato!"
|
|||||||
|
|
||||||
#: .\cookbook\views\views.py:152
|
#: .\cookbook\views\views.py:152
|
||||||
msgid "This recipe is already linked to the book!"
|
msgid "This recipe is already linked to the book!"
|
||||||
msgstr ""
|
msgstr "Questa ricetta è già collegata al libro!"
|
||||||
|
|
||||||
#: .\cookbook\views\views.py:158
|
#: .\cookbook\views\views.py:158
|
||||||
msgid "Bookmark saved!"
|
msgid "Bookmark saved!"
|
||||||
@ -1976,158 +1868,3 @@ msgstr "È stato fornito un link di invito non valido!"
|
|||||||
#: .\cookbook\views\views.py:470
|
#: .\cookbook\views\views.py:470
|
||||||
msgid "Invite Link not valid or already used!"
|
msgid "Invite Link not valid or already used!"
|
||||||
msgstr "Il link di invito non è valido o è stato già usato!"
|
msgstr "Il link di invito non è valido o è stato già usato!"
|
||||||
|
|
||||||
#~ msgid "Export Base64 encoded image?"
|
|
||||||
#~ msgstr "Esportare immagini codificate in Base64?"
|
|
||||||
|
|
||||||
#~ msgid "Download export directly or show on page?"
|
|
||||||
#~ msgstr "Scaricare l'esportazione direttamente o mostrare sulla pagina?"
|
|
||||||
|
|
||||||
#~ msgid "Simply paste a JSON export into this textarea and click import."
|
|
||||||
#~ msgstr ""
|
|
||||||
#~ "Semplicemente incolla un'esportazione JSON in questa area di testo e "
|
|
||||||
#~ "clicca su importa."
|
|
||||||
|
|
||||||
#~ msgid "Scaling factor for recipe."
|
|
||||||
#~ msgstr "Fattore di ridimensionamento per le ricette."
|
|
||||||
|
|
||||||
#~ msgid "Exported Recipe"
|
|
||||||
#~ msgstr "Ricette Esportate"
|
|
||||||
|
|
||||||
#~ msgid "Copy to clipboard"
|
|
||||||
#~ msgstr "Copia negli appunti"
|
|
||||||
|
|
||||||
#~ msgid "Copied!"
|
|
||||||
#~ msgstr "Copiato!"
|
|
||||||
|
|
||||||
#~ msgid "Copy list to clipboard"
|
|
||||||
#~ msgstr "Copia la lista negli appunti"
|
|
||||||
|
|
||||||
#~ msgid "Error"
|
|
||||||
#~ msgstr "Errore"
|
|
||||||
|
|
||||||
#~ msgid "There was an error loading the recipe!"
|
|
||||||
#~ msgstr "Si è verificato un errore durante il caricamento della ricetta!"
|
|
||||||
|
|
||||||
#~ msgid "Updated"
|
|
||||||
#~ msgstr "Caricato"
|
|
||||||
|
|
||||||
#~ msgid "Changes saved successfully!"
|
|
||||||
#~ msgstr "Cambiamenti salvati con successo!"
|
|
||||||
|
|
||||||
#~ msgid "There was an error updating the recipe!"
|
|
||||||
#~ msgstr "Si è verificato un errore durante l'aggiornamento della ricetta!"
|
|
||||||
|
|
||||||
#~ msgid "Are you sure that you want to delete this ingredient?"
|
|
||||||
#~ msgstr "Sei sicuro di voler eliminare questo ingrediente?"
|
|
||||||
|
|
||||||
#~ msgid "Are you sure that you want to delete this step?"
|
|
||||||
#~ msgstr "Sei sicuro di voler eliminare questo step?"
|
|
||||||
|
|
||||||
#~ msgid "There was an error loading a resource!"
|
|
||||||
#~ msgstr "Si è verificato un errore durante il caricamento di una risorsa!"
|
|
||||||
|
|
||||||
#~ msgid "Recipe Multiplier"
|
|
||||||
#~ msgstr "Moltiplicatore di Ricetta"
|
|
||||||
|
|
||||||
#~ msgid ""
|
|
||||||
#~ "When deleting a meal type all entries using that type will be deleted as "
|
|
||||||
#~ "well. Deletion will apply when configuration is saved. Do you want to "
|
|
||||||
#~ "proceed?"
|
|
||||||
#~ msgstr ""
|
|
||||||
#~ "Quando elimi un tipo di pasto tutte le voci che usano quel tipo verranno "
|
|
||||||
#~ "eliminate. L'eliminazione avviene quando la configurazione viene salvata. "
|
|
||||||
#~ "Vuoi procedere?"
|
|
||||||
|
|
||||||
#~ msgid "Add to Book"
|
|
||||||
#~ msgstr "Aggiungi a libro"
|
|
||||||
|
|
||||||
#~ msgid "Add to Plan"
|
|
||||||
#~ msgstr "Aggiungi a piano"
|
|
||||||
|
|
||||||
#~ msgid "Print"
|
|
||||||
#~ msgstr "Stampa"
|
|
||||||
|
|
||||||
#~ msgid "Share"
|
|
||||||
#~ msgstr "Condividi"
|
|
||||||
|
|
||||||
#~ msgid "in"
|
|
||||||
#~ msgstr "in"
|
|
||||||
|
|
||||||
#~ msgid "Preparation time ~"
|
|
||||||
#~ msgstr "Tempo di preparazione ~"
|
|
||||||
|
|
||||||
#~ msgid "Minutes"
|
|
||||||
#~ msgstr "Minuti"
|
|
||||||
|
|
||||||
#~ msgid "View external recipe"
|
|
||||||
#~ msgstr "Mostra ricetta esterna"
|
|
||||||
|
|
||||||
#~ msgid "External recipe image"
|
|
||||||
#~ msgstr "Immagine ricetta esterna"
|
|
||||||
|
|
||||||
#~ msgid "External recipe"
|
|
||||||
#~ msgstr "Ricetta Esterna"
|
|
||||||
|
|
||||||
#~ msgid ""
|
|
||||||
#~ "\n"
|
|
||||||
#~ " This is an external recipe, which "
|
|
||||||
#~ "means you can only view it by opening the link\n"
|
|
||||||
#~ " above.\n"
|
|
||||||
#~ " You can convert this recipe to a "
|
|
||||||
#~ "fancy recipe by pressing the convert button. The\n"
|
|
||||||
#~ " original\n"
|
|
||||||
#~ " file\n"
|
|
||||||
#~ " will still be accessible.\n"
|
|
||||||
#~ " "
|
|
||||||
#~ msgstr ""
|
|
||||||
#~ "\n"
|
|
||||||
#~ "Questa è una ricetta esterna, che significa puoi solo aprirla con il "
|
|
||||||
#~ "link \n"
|
|
||||||
#~ "qui sopra.\n"
|
|
||||||
#~ "Puoi convertire questa ricetta ad una più bella cliccando il tasto "
|
|
||||||
#~ "Converti.\n"
|
|
||||||
#~ "Il\n"
|
|
||||||
#~ "file \n"
|
|
||||||
#~ "originale\n"
|
|
||||||
#~ "sarà sempre accessibile."
|
|
||||||
|
|
||||||
#~ msgid "Convert now!"
|
|
||||||
#~ msgstr "Converti ora!"
|
|
||||||
|
|
||||||
#~ msgid "Your username and password didn't match. Please try again."
|
|
||||||
#~ msgstr ""
|
|
||||||
#~ "La combinazione inserita di username e password non è valida. Riprova."
|
|
||||||
|
|
||||||
#~ msgid "There was an error updating a resource!"
|
|
||||||
#~ msgstr "Si è verificato un errore durante l'aggiornamento di una risorsa!"
|
|
||||||
|
|
||||||
#~ msgid "Object created successfully!"
|
|
||||||
#~ msgstr "Oggetto creato con successo!"
|
|
||||||
|
|
||||||
#~ msgid "Please enter a valid food"
|
|
||||||
#~ msgstr "Inserisci un alimento valido"
|
|
||||||
|
|
||||||
#~ msgid "Already importing the selected recipe, please wait!"
|
|
||||||
#~ msgstr "L'importazione della ricetta selezionata è già in corso, attendere!"
|
|
||||||
|
|
||||||
#~ msgid "An error occurred while trying to import this recipe!"
|
|
||||||
#~ msgstr ""
|
|
||||||
#~ "Si è verificato un errore durante il tentativo d'importazione di questa "
|
|
||||||
#~ "ricetta!"
|
|
||||||
|
|
||||||
#~ msgid "Recipe imported successfully!"
|
|
||||||
#~ msgstr "Ricetta importata con successo!"
|
|
||||||
|
|
||||||
#~ msgid "Something went wrong during the import!"
|
|
||||||
#~ msgstr "Qualcosa è andato storto durante l'importazione!"
|
|
||||||
|
|
||||||
#~ msgid "Could not parse the supplied JSON!"
|
|
||||||
#~ msgstr "Impossibile analizzare il codice JSON!"
|
|
||||||
|
|
||||||
#~ msgid ""
|
|
||||||
#~ "External recipes cannot be exported, please share the file directly or "
|
|
||||||
#~ "select an internal recipe."
|
|
||||||
#~ msgstr ""
|
|
||||||
#~ "Le ricette esterne non possono esportate, condividi direttamente il file "
|
|
||||||
#~ "oppure seleziona una ricetta interna."
|
|
||||||
|
Binary file not shown.
@ -16,12 +16,11 @@ msgstr ""
|
|||||||
"POT-Creation-Date: 2021-02-09 18:01+0100\n"
|
"POT-Creation-Date: 2021-02-09 18:01+0100\n"
|
||||||
"PO-Revision-Date: 2020-06-02 19:28+0000\n"
|
"PO-Revision-Date: 2020-06-02 19:28+0000\n"
|
||||||
"Last-Translator: kampsj <jkamps@gmail.com>, 2021\n"
|
"Last-Translator: kampsj <jkamps@gmail.com>, 2021\n"
|
||||||
"Language-Team: Dutch (https://www.transifex.com/django-recipes/teams/110507/"
|
"Language-Team: Dutch (https://www.transifex.com/django-recipes/teams/110507/nl/)\n"
|
||||||
"nl/)\n"
|
|
||||||
"Language: nl\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Language: nl\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
#: .\cookbook\filters.py:22 .\cookbook\templates\base.html:87
|
#: .\cookbook\filters.py:22 .\cookbook\templates\base.html:87
|
||||||
@ -88,7 +87,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: .\cookbook\forms.py:55
|
#: .\cookbook\forms.py:55
|
||||||
msgid "Makes the navbar stick to the top of the page."
|
msgid "Makes the navbar stick to the top of the page."
|
||||||
msgstr ""
|
msgstr "Zet de navbar vast aan de bovenkant van de pagina."
|
||||||
|
|
||||||
#: .\cookbook\forms.py:71
|
#: .\cookbook\forms.py:71
|
||||||
msgid ""
|
msgid ""
|
||||||
@ -127,10 +126,8 @@ msgid "Storage UID"
|
|||||||
msgstr "Opslag UID"
|
msgstr "Opslag UID"
|
||||||
|
|
||||||
#: .\cookbook\forms.py:117
|
#: .\cookbook\forms.py:117
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Number of Days"
|
|
||||||
msgid "Number of servings"
|
msgid "Number of servings"
|
||||||
msgstr "Aantal dagen"
|
msgstr "Porties"
|
||||||
|
|
||||||
#: .\cookbook\forms.py:128
|
#: .\cookbook\forms.py:128
|
||||||
msgid ""
|
msgid ""
|
||||||
@ -142,7 +139,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: .\cookbook\forms.py:143
|
#: .\cookbook\forms.py:143
|
||||||
msgid "Default"
|
msgid "Default"
|
||||||
msgstr ""
|
msgstr "Standaard waarde"
|
||||||
|
|
||||||
#: .\cookbook\forms.py:162
|
#: .\cookbook\forms.py:162
|
||||||
msgid "New Unit"
|
msgid "New Unit"
|
||||||
@ -190,11 +187,11 @@ msgstr "Laat leeg voor nextcloud en vul de api token in voor dropbox."
|
|||||||
|
|
||||||
#: .\cookbook\forms.py:244
|
#: .\cookbook\forms.py:244
|
||||||
msgid ""
|
msgid ""
|
||||||
"Leave empty for dropbox and enter only base url for nextcloud (<code>/remote."
|
"Leave empty for dropbox and enter only base url for nextcloud "
|
||||||
"php/webdav/</code> is added automatically)"
|
"(<code>/remote.php/webdav/</code> is added automatically)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Laat leeg voor dropbox en vul enkel de base url voor nextcloud in. (<code>/"
|
"Laat leeg voor dropbox en vul enkel de base url voor nextcloud in. "
|
||||||
"remote.php/webdav/</code> wordt automatisch toegevoegd.)"
|
"(<code>/remote.php/webdav/</code> wordt automatisch toegevoegd.)"
|
||||||
|
|
||||||
#: .\cookbook\forms.py:263
|
#: .\cookbook\forms.py:263
|
||||||
msgid "Search String"
|
msgid "Search String"
|
||||||
@ -217,11 +214,11 @@ msgstr ""
|
|||||||
#: .\cookbook\forms.py:313
|
#: .\cookbook\forms.py:313
|
||||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:377
|
#: .\cookbook\templates\forms\edit_internal_recipe.html:377
|
||||||
msgid ""
|
msgid ""
|
||||||
"You can use markdown to format this field. See the <a href=\"/docs/markdown/"
|
"You can use markdown to format this field. See the <a "
|
||||||
"\">docs here</a>"
|
"href=\"/docs/markdown/\">docs here</a>"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Je kunt markdown gebruiken om dit veld te op te maken. Bekijk de <a href=\"/"
|
"Je kunt markdown gebruiken om dit veld te op te maken. Bekijk de <a "
|
||||||
"docs/markdown/\">documentatie hier</a>."
|
"href=\"/docs/markdown/\">documentatie hier</a>."
|
||||||
|
|
||||||
#: .\cookbook\forms.py:328
|
#: .\cookbook\forms.py:328
|
||||||
msgid "A username is not required, if left blank the new user can choose one."
|
msgid "A username is not required, if left blank the new user can choose one."
|
||||||
@ -272,6 +269,7 @@ msgid ""
|
|||||||
"Importer expected a .zip file. Did you choose the correct importer type for "
|
"Importer expected a .zip file. Did you choose the correct importer type for "
|
||||||
"your data ?"
|
"your data ?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"De importtool verwachtte een .zip bestand. Heb je het juiste type gekozen?"
|
||||||
|
|
||||||
#: .\cookbook\integration\safron.py:23
|
#: .\cookbook\integration\safron.py:23
|
||||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:65
|
#: .\cookbook\templates\forms\edit_internal_recipe.html:65
|
||||||
@ -281,8 +279,6 @@ msgid "Servings"
|
|||||||
msgstr "Porties"
|
msgstr "Porties"
|
||||||
|
|
||||||
#: .\cookbook\integration\safron.py:25
|
#: .\cookbook\integration\safron.py:25
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Waiting time ~"
|
|
||||||
msgid "Waiting time"
|
msgid "Waiting time"
|
||||||
msgstr "Wachttijd"
|
msgstr "Wachttijd"
|
||||||
|
|
||||||
@ -299,7 +295,7 @@ msgstr "Kookboek"
|
|||||||
|
|
||||||
#: .\cookbook\integration\safron.py:31
|
#: .\cookbook\integration\safron.py:31
|
||||||
msgid "Section"
|
msgid "Section"
|
||||||
msgstr ""
|
msgstr "Sectie"
|
||||||
|
|
||||||
#: .\cookbook\migrations\0047_auto_20200602_1133.py:12
|
#: .\cookbook\migrations\0047_auto_20200602_1133.py:12
|
||||||
msgid "Breakfast"
|
msgid "Breakfast"
|
||||||
@ -395,37 +391,35 @@ msgstr "Inloggen"
|
|||||||
#: .\cookbook\templates\account\login.html:13
|
#: .\cookbook\templates\account\login.html:13
|
||||||
#: .\cookbook\templates\account\login.html:28
|
#: .\cookbook\templates\account\login.html:28
|
||||||
msgid "Sign In"
|
msgid "Sign In"
|
||||||
msgstr ""
|
msgstr "Log in"
|
||||||
|
|
||||||
#: .\cookbook\templates\account\login.html:38
|
#: .\cookbook\templates\account\login.html:38
|
||||||
msgid "Social Login"
|
msgid "Social Login"
|
||||||
msgstr ""
|
msgstr "Socials login"
|
||||||
|
|
||||||
#: .\cookbook\templates\account\login.html:39
|
#: .\cookbook\templates\account\login.html:39
|
||||||
msgid "You can use any of the following providers to sign in."
|
msgid "You can use any of the following providers to sign in."
|
||||||
msgstr ""
|
msgstr "Je kan een van de volgende providers gebruiken om in te loggen."
|
||||||
|
|
||||||
#: .\cookbook\templates\account\logout.html:5
|
#: .\cookbook\templates\account\logout.html:5
|
||||||
#: .\cookbook\templates\account\logout.html:9
|
#: .\cookbook\templates\account\logout.html:9
|
||||||
#: .\cookbook\templates\account\logout.html:18
|
#: .\cookbook\templates\account\logout.html:18
|
||||||
msgid "Sign Out"
|
msgid "Sign Out"
|
||||||
msgstr ""
|
msgstr "Log uit"
|
||||||
|
|
||||||
#: .\cookbook\templates\account\logout.html:11
|
#: .\cookbook\templates\account\logout.html:11
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Are you sure that you want to merge these two units?"
|
|
||||||
msgid "Are you sure you want to sign out?"
|
msgid "Are you sure you want to sign out?"
|
||||||
msgstr "Weet je zeker dat je deze twee eenheden wil samenvoegen?"
|
msgstr "Weet je zeker dat je uit wil loggen?"
|
||||||
|
|
||||||
#: .\cookbook\templates\account\password_reset.html:5
|
#: .\cookbook\templates\account\password_reset.html:5
|
||||||
#: .\cookbook\templates\account\password_reset_done.html:5
|
#: .\cookbook\templates\account\password_reset_done.html:5
|
||||||
msgid "Password Reset"
|
msgid "Password Reset"
|
||||||
msgstr ""
|
msgstr "Wachtwoord reset"
|
||||||
|
|
||||||
#: .\cookbook\templates\account\password_reset.html:9
|
#: .\cookbook\templates\account\password_reset.html:9
|
||||||
#: .\cookbook\templates\account\password_reset_done.html:9
|
#: .\cookbook\templates\account\password_reset_done.html:9
|
||||||
msgid "Password reset is not implemented for the time being!"
|
msgid "Password reset is not implemented for the time being!"
|
||||||
msgstr ""
|
msgstr "Wachtwoord reset is nog niet geïmplementeerd!"
|
||||||
|
|
||||||
#: .\cookbook\templates\account\signup.html:5
|
#: .\cookbook\templates\account\signup.html:5
|
||||||
msgid "Register"
|
msgid "Register"
|
||||||
@ -643,20 +637,16 @@ msgid "Waiting Time"
|
|||||||
msgstr "Wachttijd"
|
msgstr "Wachttijd"
|
||||||
|
|
||||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:68
|
#: .\cookbook\templates\forms\edit_internal_recipe.html:68
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Servings"
|
|
||||||
msgid "Servings Text"
|
msgid "Servings Text"
|
||||||
msgstr "Porties"
|
msgstr "Porties tekst"
|
||||||
|
|
||||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:79
|
#: .\cookbook\templates\forms\edit_internal_recipe.html:79
|
||||||
msgid "Select Keywords"
|
msgid "Select Keywords"
|
||||||
msgstr "Selecteer sleutelwoorden"
|
msgstr "Selecteer sleutelwoorden"
|
||||||
|
|
||||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:93
|
#: .\cookbook\templates\forms\edit_internal_recipe.html:93
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Nutrition"
|
|
||||||
msgid "Description"
|
msgid "Description"
|
||||||
msgstr "Voedingswaarde"
|
msgstr "Beschrijving"
|
||||||
|
|
||||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:108
|
#: .\cookbook\templates\forms\edit_internal_recipe.html:108
|
||||||
msgid "Nutrition"
|
msgid "Nutrition"
|
||||||
@ -772,7 +762,7 @@ msgstr "Hoeveelheid inschakelen"
|
|||||||
|
|
||||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:348
|
#: .\cookbook\templates\forms\edit_internal_recipe.html:348
|
||||||
msgid "Copy Template Reference"
|
msgid "Copy Template Reference"
|
||||||
msgstr ""
|
msgstr "Kopieer sjabloon referentie"
|
||||||
|
|
||||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:374
|
#: .\cookbook\templates\forms\edit_internal_recipe.html:374
|
||||||
#: .\cookbook\templates\url_import.html:177
|
#: .\cookbook\templates\url_import.html:177
|
||||||
@ -820,18 +810,14 @@ msgstr "Ingrediënten bewerken"
|
|||||||
#: .\cookbook\templates\forms\ingredients.html:16
|
#: .\cookbook\templates\forms\ingredients.html:16
|
||||||
msgid ""
|
msgid ""
|
||||||
"\n"
|
"\n"
|
||||||
" The following form can be used if, accidentally, two (or more) units "
|
" The following form can be used if, accidentally, two (or more) units or ingredients where created that should be\n"
|
||||||
"or ingredients where created that should be\n"
|
|
||||||
" the same.\n"
|
" the same.\n"
|
||||||
" It merges two units or ingredients and updates all recipes using "
|
" It merges two units or ingredients and updates all recipes using them.\n"
|
||||||
"them.\n"
|
|
||||||
" "
|
" "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
"Het volgende formulier kan worden gebruikt wanneer per ongeluk twee (of "
|
"Het volgende formulier kan worden gebruikt wanneer per ongeluk twee (of meer) eenheden of ingrediënten zijn gecreëerd dat eigenlijk hetzelfde zijn.\n"
|
||||||
"meer) eenheden of ingrediënten zijn gecreëerd dat eigenlijk hetzelfde zijn.\n"
|
"Het doet de twee eenheden of ingrediënten samenvoegen en alle bijbehorende recepten updaten."
|
||||||
"Het doet de twee eenheden of ingrediënten samenvoegen en alle bijbehorende "
|
|
||||||
"recepten updaten."
|
|
||||||
|
|
||||||
#: .\cookbook\templates\forms\ingredients.html:24
|
#: .\cookbook\templates\forms\ingredients.html:24
|
||||||
#: .\cookbook\templates\stats.html:26
|
#: .\cookbook\templates\stats.html:26
|
||||||
@ -953,22 +939,16 @@ msgstr "Veiligheidswaarschuwing"
|
|||||||
#: .\cookbook\templates\include\storage_backend_warning.html:5
|
#: .\cookbook\templates\include\storage_backend_warning.html:5
|
||||||
msgid ""
|
msgid ""
|
||||||
"\n"
|
"\n"
|
||||||
" The <b>Password and Token</b> field are stored as <b>plain text</b> "
|
" The <b>Password and Token</b> field are stored as <b>plain text</b> inside the database.\n"
|
||||||
"inside the database.\n"
|
" This is necessary because they are needed to make API requests, but it also increases the risk of\n"
|
||||||
" This is necessary because they are needed to make API requests, but "
|
|
||||||
"it also increases the risk of\n"
|
|
||||||
" someone stealing it. <br/>\n"
|
" someone stealing it. <br/>\n"
|
||||||
" To limit the possible damage tokens or accounts with limited access "
|
" To limit the possible damage tokens or accounts with limited access can be used.\n"
|
||||||
"can be used.\n"
|
|
||||||
" "
|
" "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
"Het <b>wachtwoord en token</b> veld worden als <b>plain text</b> in de "
|
"Het <b>wachtwoord en token</b> veld worden als <b>plain text</b> in de database opgeslagen.\n"
|
||||||
"database opgeslagen.\n"
|
"Dit is benodigd omdat deze benodigd zijn voor de API requests, Echter verhoogd dit ook het risico van diefstal.<br/>\n"
|
||||||
"Dit is benodigd omdat deze benodigd zijn voor de API requests, Echter "
|
"Om mogelijke schade te beperken kunt u gebruik maken van account met gelimiteerde toegang."
|
||||||
"verhoogd dit ook het risico van diefstal.<br/>\n"
|
|
||||||
"Om mogelijke schade te beperken kunt u gebruik maken van account met "
|
|
||||||
"gelimiteerde toegang."
|
|
||||||
|
|
||||||
#: .\cookbook\templates\index.html:29
|
#: .\cookbook\templates\index.html:29
|
||||||
msgid "Search recipe ..."
|
msgid "Search recipe ..."
|
||||||
@ -1011,26 +991,17 @@ msgstr "Markdown informatie"
|
|||||||
#: .\cookbook\templates\markdown_info.html:14
|
#: .\cookbook\templates\markdown_info.html:14
|
||||||
msgid ""
|
msgid ""
|
||||||
"\n"
|
"\n"
|
||||||
" Markdown is lightweight markup language that can be used to format "
|
" Markdown is lightweight markup language that can be used to format plain text easily.\n"
|
||||||
"plain text easily.\n"
|
" This site uses the <a href=\"https://python-markdown.github.io/\" target=\"_blank\">Python Markdown</a> library to\n"
|
||||||
" This site uses the <a href=\"https://python-markdown.github.io/\" "
|
" convert your text into nice looking HTML. Its full markdown documentation can be found\n"
|
||||||
"target=\"_blank\">Python Markdown</a> library to\n"
|
" <a href=\"https://daringfireball.net/projects/markdown/syntax\" target=\"_blank\">here</a>.\n"
|
||||||
" convert your text into nice looking HTML. Its full markdown "
|
" An incomplete but most likely sufficient documentation can be found below.\n"
|
||||||
"documentation can be found\n"
|
|
||||||
" <a href=\"https://daringfireball.net/projects/markdown/syntax\" "
|
|
||||||
"target=\"_blank\">here</a>.\n"
|
|
||||||
" An incomplete but most likely sufficient documentation can be found "
|
|
||||||
"below.\n"
|
|
||||||
" "
|
" "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
"Markdown is een lichtgewicht opmaak taal die gebruikt kan worden om tekst "
|
"Markdown is een lichtgewicht opmaak taal die gebruikt kan worden om tekst eenvoudig op te maken.\n"
|
||||||
"eenvoudig op te maken.\n"
|
"Deze site gebruikt de <a href=\"https://python-markdown.github.io/\" target=\"_blank\">Python Markdown</a> bibliotheek\n"
|
||||||
"Deze site gebruikt de <a href=\"https://python-markdown.github.io/\" target="
|
"om je tekst in mooi uitziende HTML om te zetten. De volledige documentatie kan <a href=\"https://daringfireball.net/projects/markdown/syntax\" target=\"_blank\">hier</a>gevonden worden.\n"
|
||||||
"\"_blank\">Python Markdown</a> bibliotheek\n"
|
|
||||||
"om je tekst in mooi uitziende HTML om te zetten. De volledige documentatie "
|
|
||||||
"kan <a href=\"https://daringfireball.net/projects/markdown/syntax\" target="
|
|
||||||
"\"_blank\">hier</a>gevonden worden.\n"
|
|
||||||
"Onvolledige, maar waarschijnlijk voldoende, informatie staat hieronder."
|
"Onvolledige, maar waarschijnlijk voldoende, informatie staat hieronder."
|
||||||
|
|
||||||
#: .\cookbook\templates\markdown_info.html:25
|
#: .\cookbook\templates\markdown_info.html:25
|
||||||
@ -1130,19 +1101,15 @@ msgid "Tables"
|
|||||||
msgstr "Tabellen"
|
msgstr "Tabellen"
|
||||||
|
|
||||||
#: .\cookbook\templates\markdown_info.html:153
|
#: .\cookbook\templates\markdown_info.html:153
|
||||||
#, fuzzy
|
|
||||||
#| msgid ""
|
|
||||||
#| "Markdown tables are hard to create by hand. It is recommended to use a "
|
|
||||||
#| "table editor like <a href=\"https://www.tablesgenerator.com/"
|
|
||||||
#| "markdown_tables\" target=\"_blank\">this</a> one."
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"Markdown tables are hard to create by hand. It is recommended to use a table"
|
"Markdown tables are hard to create by hand. It is recommended to use a table"
|
||||||
"editor like <a href=\"https://www.tablesgenerator.com/markdown_tables\" rel="
|
" editor like <a href=\"https://www.tablesgenerator.com/markdown_tables\" "
|
||||||
"\"noreferrer noopener\" target=\"_blank\">this one.</a>"
|
"rel=\"noreferrer noopener\" target=\"_blank\">this one.</a>"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Het is lastig om markdown tabellen handmatig te creëren. Het is geadviseerd "
|
"Het is lastig om met de hand Markdown tabellen te maken. Het wordt "
|
||||||
"dat u een tabel bewerker zoals <a href=\"https://www.tablesgenerator.com/"
|
"aangeraden om een tabel editor zoals <a "
|
||||||
"markdown_tables\" target=\"_blank\">deze</a> gebruikt."
|
"href=\"https://www.tablesgenerator.com/markdown_tables\" rel=\"noreferrer "
|
||||||
|
"noopener\" target=\"_blank\">deze</a> te gebruiken."
|
||||||
|
|
||||||
#: .\cookbook\templates\markdown_info.html:155
|
#: .\cookbook\templates\markdown_info.html:155
|
||||||
#: .\cookbook\templates\markdown_info.html:157
|
#: .\cookbook\templates\markdown_info.html:157
|
||||||
@ -1180,18 +1147,18 @@ msgstr "Notitie (optioneel)"
|
|||||||
|
|
||||||
#: .\cookbook\templates\meal_plan.html:143
|
#: .\cookbook\templates\meal_plan.html:143
|
||||||
msgid ""
|
msgid ""
|
||||||
"You can use markdown to format this field. See the <a href=\"/docs/markdown/"
|
"You can use markdown to format this field. See the <a "
|
||||||
"\" target=\"_blank\" rel=\"noopener noreferrer\">docs here</a>"
|
"href=\"/docs/markdown/\" target=\"_blank\" rel=\"noopener noreferrer\">docs "
|
||||||
|
"here</a>"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Je kan markdown gebruiken om dit veld op te maken. Zie de <a href=\"/docs/"
|
"Je kan markdown gebruiken om dit veld op te maken. Zie de <a "
|
||||||
"markdown/\" target=\"_blank\" rel=\"noopener noreferrer\">documentatie</a>"
|
"href=\"/docs/markdown/\" target=\"_blank\" rel=\"noopener "
|
||||||
|
"noreferrer\">documentatie</a>"
|
||||||
|
|
||||||
#: .\cookbook\templates\meal_plan.html:147
|
#: .\cookbook\templates\meal_plan.html:147
|
||||||
#: .\cookbook\templates\meal_plan.html:251
|
#: .\cookbook\templates\meal_plan.html:251
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Servings"
|
|
||||||
msgid "Serving Count"
|
msgid "Serving Count"
|
||||||
msgstr "Porties"
|
msgstr "Portie teller"
|
||||||
|
|
||||||
#: .\cookbook\templates\meal_plan.html:153
|
#: .\cookbook\templates\meal_plan.html:153
|
||||||
msgid "Create only note"
|
msgid "Create only note"
|
||||||
@ -1269,94 +1236,37 @@ msgid "Meal Plan Help"
|
|||||||
msgstr "Maaltijdplanner hulp"
|
msgstr "Maaltijdplanner hulp"
|
||||||
|
|
||||||
#: .\cookbook\templates\meal_plan.html:344
|
#: .\cookbook\templates\meal_plan.html:344
|
||||||
#, fuzzy
|
|
||||||
#| msgid ""
|
|
||||||
#| "\n"
|
|
||||||
#| " <p>The meal plan module allows planning of "
|
|
||||||
#| "meals both with recipes or just notes.</p>\n"
|
|
||||||
#| " <p>Simply select a recipe from the list of "
|
|
||||||
#| "recently viewed recipes or search the one you\n"
|
|
||||||
#| " want and drag it to the desired plan "
|
|
||||||
#| "position. You can also add a note and a title and\n"
|
|
||||||
#| " then drag the recipe to create a plan "
|
|
||||||
#| "entry with a custom title and note. Creating only\n"
|
|
||||||
#| " Notes is possible by dragging the create "
|
|
||||||
#| "note box into the plan.</p>\n"
|
|
||||||
#| " <p>Click on a recipe in order to open the "
|
|
||||||
#| "detail view. Here you can also add it to the\n"
|
|
||||||
#| " shopping list. You can also add all "
|
|
||||||
#| "recipes of a day to the shopping list by\n"
|
|
||||||
#| " clicking the shopping cart at the top of "
|
|
||||||
#| "the table.</p>\n"
|
|
||||||
#| " <p>Since a common use case is to plan meals "
|
|
||||||
#| "together you can define\n"
|
|
||||||
#| " users you want to share your plan with in "
|
|
||||||
#| "the settings.\n"
|
|
||||||
#| " </p>\n"
|
|
||||||
#| " <p>You can also edit the types of meals you "
|
|
||||||
#| "want to plan. If you share your plan with\n"
|
|
||||||
#| " someone with\n"
|
|
||||||
#| " different meals, their meal types will "
|
|
||||||
#| "appear in your list as well. To prevent\n"
|
|
||||||
#| " duplicates (e.g. Other and Misc.)\n"
|
|
||||||
#| " name your meal types the same as the "
|
|
||||||
#| "users you share your meals with and they will be\n"
|
|
||||||
#| " merged.</p>\n"
|
|
||||||
#| " "
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"\n"
|
"\n"
|
||||||
" <p>The meal plan module allows planning of meals "
|
" <p>The meal plan module allows planning of meals both with recipes and notes.</p>\n"
|
||||||
"both with recipes and notes.</p>\n"
|
" <p>Simply select a recipe from the list of recently viewed recipes or search the one you\n"
|
||||||
" <p>Simply select a recipe from the list of "
|
" want and drag it to the desired plan position. You can also add a note and a title and\n"
|
||||||
"recently viewed recipes or search the one you\n"
|
" then drag the recipe to create a plan entry with a custom title and note. Creating only\n"
|
||||||
" want and drag it to the desired plan "
|
" Notes is possible by dragging the create note box into the plan.</p>\n"
|
||||||
"position. You can also add a note and a title and\n"
|
" <p>Click on a recipe in order to open the detailed view. There you can also add it to the\n"
|
||||||
" then drag the recipe to create a plan entry "
|
" shopping list. You can also add all recipes of a day to the shopping list by\n"
|
||||||
"with a custom title and note. Creating only\n"
|
" clicking the shopping cart at the top of the table.</p>\n"
|
||||||
" Notes is possible by dragging the create "
|
" <p>Since a common use case is to plan meals together you can define\n"
|
||||||
"note box into the plan.</p>\n"
|
" users you want to share your plan with in the settings.\n"
|
||||||
" <p>Click on a recipe in order to open the "
|
|
||||||
"detailed view. There you can also add it to the\n"
|
|
||||||
" shopping list. You can also add all recipes "
|
|
||||||
"of a day to the shopping list by\n"
|
|
||||||
" clicking the shopping cart at the top of the "
|
|
||||||
"table.</p>\n"
|
|
||||||
" <p>Since a common use case is to plan meals "
|
|
||||||
"together you can define\n"
|
|
||||||
" users you want to share your plan with in "
|
|
||||||
"the settings.\n"
|
|
||||||
" </p>\n"
|
" </p>\n"
|
||||||
" <p>You can also edit the types of meals you want "
|
" <p>You can also edit the types of meals you want to plan. If you share your plan with\n"
|
||||||
"to plan. If you share your plan with\n"
|
|
||||||
" someone with\n"
|
" someone with\n"
|
||||||
" different meals, their meal types will "
|
" different meals, their meal types will appear in your list as well. To prevent\n"
|
||||||
"appear in your list as well. To prevent\n"
|
|
||||||
" duplicates (e.g. Other and Misc.)\n"
|
" duplicates (e.g. Other and Misc.)\n"
|
||||||
" name your meal types the same as the users "
|
" name your meal types the same as the users you share your meals with and they will be\n"
|
||||||
"you share your meals with and they will be\n"
|
|
||||||
" merged.</p>\n"
|
" merged.</p>\n"
|
||||||
" "
|
" "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
"<p>De maaltijdplanner maakt het mogelijk maaltijden op basis van recepten of "
|
"<p>De maaltijdplan module maakt plannen van maaltijden met recepten en notities mogelijk.</p>\n"
|
||||||
"notities te plannen.</p>\n"
|
"<p>Selecteer een recept van de lijst recent bekeken recepten of zoek naar\n"
|
||||||
"<p>Selecteer een recept uit de lijst van recent bekeken recepten of zoek het "
|
"het gewenste recept en sleep het naar de juiste positie in het maaltijdplan. Je kan ook eerst een notitie en titel toevoegen en dan het recept naar de juiste positie slepen om een unieke maaltijdplan inschrijving te maken.\n"
|
||||||
"recept dat je wil en sleep het naar de gewenste positie. Je kan ook eerst "
|
"Alleen notities aanmaken is mogelijk door het Maak notitie vlak in het maaltijdplan te slepen.</p>\n"
|
||||||
"een notitie en titel toevoegen en dan het recept naar de gewenste positie "
|
"<p>Klik op een recept om de gedetailleerde weergave te openen. Daar kan je het ook toevoegen aan je boodschappenlijst.\n"
|
||||||
"slepen om een maaltijdplan met een aangepaste titel en notitie te maken. "
|
"Je kan ook alle recepten van een dag aan je boodschappenlijst toevoegen door op het winkelwagentje boven aan de tabel te klikken.</p>\n"
|
||||||
"Alleen notities aanmaken is ook mogelijk door 'Maak notitie' in het "
|
"<p>Omdat maaltijden samen gepland kunnen worden kan je in de instellingen kiezen met welke gebruikers je je maaltijd plan wil delen.\n"
|
||||||
"maaltijdplan te slepen.</p>\n"
|
"</p>\n"
|
||||||
"<p>Klik op een recept om het te openen en de details te bekijken. Hier kan "
|
"<p>Je kan ook het type maaltijd dat je wil plannen bewerken. Als je een maaltijdplan deelt met iemand met andere maaltijden, dan zullen hun maaltijdtypes ook in jouw lijst verschijnen. Geef, om dubbelingen (zoals Overig en Anders) te voorkomen, je maaltijdtypes daarom dezelfde naam als de gebruikers waarmee je maaltijdplannen deelt. In dat geval worden de maaltijden samengevoegd.</p>"
|
||||||
"je het ook aan de boodschappenlijst toevoegen door op het winkelwagentje "
|
|
||||||
"bovenaan de tabel te klikken.</p>\n"
|
|
||||||
"<p>Omdat maaltijden vaak gezamenlijk worden gepland kan je in de "
|
|
||||||
"instellingen gebruikers aangeven met wie je het maaltijdplan wil delen.</p>\n"
|
|
||||||
"<p>Je kan ook de soort maaltijden die je wil plannen bewerken. Als je jouw "
|
|
||||||
"plan deelt met iemand met andere soorten, dan zullen deze ook in jouw lijst "
|
|
||||||
"verschijnen. Gelijknamige soorten worden samengevoegd. Zorg er daarom voor "
|
|
||||||
"dat de gebruikte soorten overeenkomen met de gebruiker met wie je je "
|
|
||||||
"maaltijdplannen deelt. Dit voorkomt dubbelingen (zoals Overige en "
|
|
||||||
"Willekeurig).</p>"
|
|
||||||
|
|
||||||
#: .\cookbook\templates\meal_plan_entry.html:6
|
#: .\cookbook\templates\meal_plan_entry.html:6
|
||||||
msgid "Meal Plan View"
|
msgid "Meal Plan View"
|
||||||
@ -1373,27 +1283,31 @@ msgstr "Andere maaltijden op deze dag"
|
|||||||
#: .\cookbook\templates\no_groups_info.html:5
|
#: .\cookbook\templates\no_groups_info.html:5
|
||||||
#: .\cookbook\templates\offline.html:6
|
#: .\cookbook\templates\offline.html:6
|
||||||
msgid "Offline"
|
msgid "Offline"
|
||||||
msgstr ""
|
msgstr "Offline"
|
||||||
|
|
||||||
#: .\cookbook\templates\no_groups_info.html:12
|
#: .\cookbook\templates\no_groups_info.html:12
|
||||||
msgid "No Permissions"
|
msgid "No Permissions"
|
||||||
msgstr ""
|
msgstr "Geen rechten"
|
||||||
|
|
||||||
#: .\cookbook\templates\no_groups_info.html:15
|
#: .\cookbook\templates\no_groups_info.html:15
|
||||||
msgid ""
|
msgid ""
|
||||||
"You do not have any groups and therefor cannot use this application. Please "
|
"You do not have any groups and therefor cannot use this application. Please "
|
||||||
"contact your administrator."
|
"contact your administrator."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Je hebt geen groepen en kan daarom deze applicatie niet gebruiken. Neem "
|
||||||
|
"contact op met je beheerder."
|
||||||
|
|
||||||
#: .\cookbook\templates\offline.html:19
|
#: .\cookbook\templates\offline.html:19
|
||||||
msgid "You are currently offline!"
|
msgid "You are currently offline!"
|
||||||
msgstr ""
|
msgstr "Je bent op dit moment offline!"
|
||||||
|
|
||||||
#: .\cookbook\templates\offline.html:20
|
#: .\cookbook\templates\offline.html:20
|
||||||
msgid ""
|
msgid ""
|
||||||
"The recipes listed below are available for offline viewing because you have "
|
"The recipes listed below are available for offline viewing because you have "
|
||||||
"recently viewed them. Keep in mind that data might be outdated."
|
"recently viewed them. Keep in mind that data might be outdated."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"De recepten hieronder zijn beschikbaar om offline te bekijken omdat je ze "
|
||||||
|
"recent bekeken hebt. Houd er rekening mee dat de data mogelijk verouderd is."
|
||||||
|
|
||||||
#: .\cookbook\templates\recipe_view.html:21 .\cookbook\templates\stats.html:47
|
#: .\cookbook\templates\recipe_view.html:21 .\cookbook\templates\stats.html:47
|
||||||
msgid "Comments"
|
msgid "Comments"
|
||||||
@ -1438,7 +1352,7 @@ msgstr "Account"
|
|||||||
|
|
||||||
#: .\cookbook\templates\settings.html:38
|
#: .\cookbook\templates\settings.html:38
|
||||||
msgid "Link social account"
|
msgid "Link social account"
|
||||||
msgstr ""
|
msgstr "Koppel account socials"
|
||||||
|
|
||||||
#: .\cookbook\templates\settings.html:42
|
#: .\cookbook\templates\settings.html:42
|
||||||
msgid "Language"
|
msgid "Language"
|
||||||
@ -1484,7 +1398,8 @@ msgstr "Setup"
|
|||||||
msgid ""
|
msgid ""
|
||||||
"To start using this application you must first create a superuser account."
|
"To start using this application you must first create a superuser account."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Om te starten met de applicatie moet je eerst een superuser account aanmaken."
|
"Om te starten met de applicatie moet je eerst een superuser account "
|
||||||
|
"aanmaken."
|
||||||
|
|
||||||
#: .\cookbook\templates\setup.html:20
|
#: .\cookbook\templates\setup.html:20
|
||||||
msgid "Create Superuser account"
|
msgid "Create Superuser account"
|
||||||
@ -1500,13 +1415,11 @@ msgstr "Geen recepten geselecteerd"
|
|||||||
|
|
||||||
#: .\cookbook\templates\shopping_list.html:145
|
#: .\cookbook\templates\shopping_list.html:145
|
||||||
msgid "Entry Mode"
|
msgid "Entry Mode"
|
||||||
msgstr ""
|
msgstr "Invoermodus"
|
||||||
|
|
||||||
#: .\cookbook\templates\shopping_list.html:153
|
#: .\cookbook\templates\shopping_list.html:153
|
||||||
#, fuzzy
|
|
||||||
#| msgid "New Entry"
|
|
||||||
msgid "Add Entry"
|
msgid "Add Entry"
|
||||||
msgstr "Nieuw item"
|
msgstr "Zet op lijst"
|
||||||
|
|
||||||
#: .\cookbook\templates\shopping_list.html:168
|
#: .\cookbook\templates\shopping_list.html:168
|
||||||
msgid "Amount"
|
msgid "Amount"
|
||||||
@ -1514,13 +1427,11 @@ msgstr "Hoeveelheid"
|
|||||||
|
|
||||||
#: .\cookbook\templates\shopping_list.html:224
|
#: .\cookbook\templates\shopping_list.html:224
|
||||||
msgid "Supermarket"
|
msgid "Supermarket"
|
||||||
msgstr ""
|
msgstr "Supermarkt"
|
||||||
|
|
||||||
#: .\cookbook\templates\shopping_list.html:234
|
#: .\cookbook\templates\shopping_list.html:234
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Select User"
|
|
||||||
msgid "Select Supermarket"
|
msgid "Select Supermarket"
|
||||||
msgstr "Selecteer gebruiker"
|
msgstr "Selecteer supermarkt"
|
||||||
|
|
||||||
#: .\cookbook\templates\shopping_list.html:258
|
#: .\cookbook\templates\shopping_list.html:258
|
||||||
msgid "Select User"
|
msgid "Select User"
|
||||||
@ -1549,26 +1460,28 @@ msgstr "Er is een fout opgetreden bij het maken van een hulpbron!"
|
|||||||
#: .\cookbook\templates\socialaccount\connections.html:4
|
#: .\cookbook\templates\socialaccount\connections.html:4
|
||||||
#: .\cookbook\templates\socialaccount\connections.html:7
|
#: .\cookbook\templates\socialaccount\connections.html:7
|
||||||
msgid "Account Connections"
|
msgid "Account Connections"
|
||||||
msgstr ""
|
msgstr "Account verbindingen"
|
||||||
|
|
||||||
#: .\cookbook\templates\socialaccount\connections.html:10
|
#: .\cookbook\templates\socialaccount\connections.html:10
|
||||||
msgid ""
|
msgid ""
|
||||||
"You can sign in to your account using any of the following third party\n"
|
"You can sign in to your account using any of the following third party\n"
|
||||||
" accounts:"
|
" accounts:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Je kan inloggen met een account van een van de onderstaande derde partijen:"
|
||||||
|
|
||||||
#: .\cookbook\templates\socialaccount\connections.html:36
|
#: .\cookbook\templates\socialaccount\connections.html:36
|
||||||
msgid "Remove"
|
msgid "Remove"
|
||||||
msgstr ""
|
msgstr "Verwijder"
|
||||||
|
|
||||||
#: .\cookbook\templates\socialaccount\connections.html:44
|
#: .\cookbook\templates\socialaccount\connections.html:44
|
||||||
msgid ""
|
msgid ""
|
||||||
"You currently have no social network accounts connected to this account."
|
"You currently have no social network accounts connected to this account."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Je hebt op dit moment geen sociaalnetwerk account aan dit account gekoppeld."
|
||||||
|
|
||||||
#: .\cookbook\templates\socialaccount\connections.html:47
|
#: .\cookbook\templates\socialaccount\connections.html:47
|
||||||
msgid "Add a 3rd Party Account"
|
msgid "Add a 3rd Party Account"
|
||||||
msgstr ""
|
msgstr "Voeg account van een 3e partij toe"
|
||||||
|
|
||||||
#: .\cookbook\templates\stats.html:4
|
#: .\cookbook\templates\stats.html:4
|
||||||
msgid "Stats"
|
msgid "Stats"
|
||||||
@ -1621,19 +1534,15 @@ msgstr "Systeeminformatie"
|
|||||||
#: .\cookbook\templates\system.html:51
|
#: .\cookbook\templates\system.html:51
|
||||||
msgid ""
|
msgid ""
|
||||||
"\n"
|
"\n"
|
||||||
" Django Recipes is an open source free software application. It can "
|
" Django Recipes is an open source free software application. It can be found on\n"
|
||||||
"be found on\n"
|
|
||||||
" <a href=\"https://github.com/vabene1111/recipes\">GitHub</a>.\n"
|
" <a href=\"https://github.com/vabene1111/recipes\">GitHub</a>.\n"
|
||||||
" Changelogs can be found <a href=\"https://github.com/vabene1111/"
|
" Changelogs can be found <a href=\"https://github.com/vabene1111/recipes/releases\">here</a>.\n"
|
||||||
"recipes/releases\">here</a>.\n"
|
|
||||||
" "
|
" "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
"Django Recipes is een open source gratis software applicatie. Het kan "
|
"Django Recipes is een open source gratis software applicatie. Het kan gevonden worden op\n"
|
||||||
"gevonden worden op\n"
|
|
||||||
"<a href=\"https://github.com/vabene1111/recipes\">GitHub</a>.\n"
|
"<a href=\"https://github.com/vabene1111/recipes\">GitHub</a>.\n"
|
||||||
"Wijzigingenoverzichten kunnen <a href=\"https://github.com/vabene1111/"
|
"Wijzigingenoverzichten kunnen <a href=\"https://github.com/vabene1111/recipes/releases\">hier</a> gevonden worden."
|
||||||
"recipes/releases\">hier</a> gevonden worden."
|
|
||||||
|
|
||||||
#: .\cookbook\templates\system.html:65
|
#: .\cookbook\templates\system.html:65
|
||||||
msgid "Media Serving"
|
msgid "Media Serving"
|
||||||
@ -1653,15 +1562,12 @@ msgstr "Ok"
|
|||||||
msgid ""
|
msgid ""
|
||||||
"Serving media files directly using gunicorn/python is <b>not recommend</b>!\n"
|
"Serving media files directly using gunicorn/python is <b>not recommend</b>!\n"
|
||||||
" Please follow the steps described\n"
|
" Please follow the steps described\n"
|
||||||
" <a href=\"https://github.com/vabene1111/recipes/releases/"
|
" <a href=\"https://github.com/vabene1111/recipes/releases/tag/0.8.1\">here</a> to update\n"
|
||||||
"tag/0.8.1\">here</a> to update\n"
|
|
||||||
" your installation.\n"
|
" your installation.\n"
|
||||||
" "
|
" "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Mediabestanden rechtstreeks aanbieden met gunicorn/python is <b>niet "
|
"Mediabestanden rechtstreeks aanbieden met gunicorn/python is <b>niet aanbevolen</b>!\n"
|
||||||
"aanbevolen</b>!\n"
|
"Volg de stappen zoals <a href=\"https://github.com/vabene1111/recipes/releases/tag/0.8.1\">hier</a> beschreven om je installatie te updaten."
|
||||||
"Volg de stappen zoals <a href=\"https://github.com/vabene1111/recipes/"
|
|
||||||
"releases/tag/0.8.1\">hier</a> beschreven om je installatie te updaten."
|
|
||||||
|
|
||||||
#: .\cookbook\templates\system.html:74 .\cookbook\templates\system.html:90
|
#: .\cookbook\templates\system.html:74 .\cookbook\templates\system.html:90
|
||||||
#: .\cookbook\templates\system.html:105 .\cookbook\templates\system.html:119
|
#: .\cookbook\templates\system.html:105 .\cookbook\templates\system.html:119
|
||||||
@ -1675,20 +1581,15 @@ msgstr "Geheime sleutel"
|
|||||||
#: .\cookbook\templates\system.html:83
|
#: .\cookbook\templates\system.html:83
|
||||||
msgid ""
|
msgid ""
|
||||||
"\n"
|
"\n"
|
||||||
" You do not have a <code>SECRET_KEY</code> configured in your "
|
" You do not have a <code>SECRET_KEY</code> configured in your <code>.env</code> file. Django defaulted to the\n"
|
||||||
"<code>.env</code> file. Django defaulted to the\n"
|
|
||||||
" standard key\n"
|
" standard key\n"
|
||||||
" provided with the installation which is publicly know and "
|
" provided with the installation which is publicly know and insecure! Please set\n"
|
||||||
"insecure! Please set\n"
|
" <code>SECRET_KEY</code> int the <code>.env</code> configuration file.\n"
|
||||||
" <code>SECRET_KEY</code> int the <code>.env</code> configuration "
|
|
||||||
"file.\n"
|
|
||||||
" "
|
" "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
"Je hebt geen <code>SECRET_KEY</code> geconfigureerd in je .env bestand.\n"
|
"Je hebt geen <code>SECRET_KEY</code> geconfigureerd in je .env bestand.\n"
|
||||||
"Django is overgegaan naar de standaard sleutel die openbaar en onveilig is! "
|
"Django is overgegaan naar de standaard sleutel die openbaar en onveilig is! Stel alsjeblieft <code>SECRET_KEY</code>in in het <code>.env</code> configuratiebestand."
|
||||||
"Stel alsjeblieft <code>SECRET_KEY</code>in in het <code>.env</code> "
|
|
||||||
"configuratiebestand."
|
|
||||||
|
|
||||||
#: .\cookbook\templates\system.html:95
|
#: .\cookbook\templates\system.html:95
|
||||||
msgid "Debug Mode"
|
msgid "Debug Mode"
|
||||||
@ -1697,17 +1598,13 @@ msgstr "Debug modus"
|
|||||||
#: .\cookbook\templates\system.html:99
|
#: .\cookbook\templates\system.html:99
|
||||||
msgid ""
|
msgid ""
|
||||||
"\n"
|
"\n"
|
||||||
" This application is still running in debug mode. This is most "
|
" This application is still running in debug mode. This is most likely not needed. Turn of debug mode by\n"
|
||||||
"likely not needed. Turn of debug mode by\n"
|
|
||||||
" setting\n"
|
" setting\n"
|
||||||
" <code>DEBUG=0</code> int the <code>.env</code> configuration "
|
" <code>DEBUG=0</code> int the <code>.env</code> configuration file.\n"
|
||||||
"file.\n"
|
|
||||||
" "
|
" "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
"Deze applicatie draait in debug modus. Dit is waarschijnlijk niet nodig. "
|
"Deze applicatie draait in debug modus. Dit is waarschijnlijk niet nodig. Schakel debug modus uit door de instelling <code>DEBUG=0</code> in het <code>.env</code>configuratiebestand aan te passen."
|
||||||
"Schakel debug modus uit door de instelling <code>DEBUG=0</code> in het "
|
|
||||||
"<code>.env</code>configuratiebestand aan te passen."
|
|
||||||
|
|
||||||
#: .\cookbook\templates\system.html:110
|
#: .\cookbook\templates\system.html:110
|
||||||
msgid "Database"
|
msgid "Database"
|
||||||
@ -1720,15 +1617,12 @@ msgstr "Info"
|
|||||||
#: .\cookbook\templates\system.html:114
|
#: .\cookbook\templates\system.html:114
|
||||||
msgid ""
|
msgid ""
|
||||||
"\n"
|
"\n"
|
||||||
" This application is not running with a Postgres database "
|
" This application is not running with a Postgres database backend. This is ok but not recommended as some\n"
|
||||||
"backend. This is ok but not recommended as some\n"
|
|
||||||
" features only work with postgres databases.\n"
|
" features only work with postgres databases.\n"
|
||||||
" "
|
" "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
"Deze applicatie draait niet met een Postgres database als backend. Dit is ok "
|
"Deze applicatie draait niet met een Postgres database als backend. Dit is ok maar wordt niet aanbevolen omdat sommige functies alleen werken met Postgres databases."
|
||||||
"maar wordt niet aanbevolen omdat sommige functies alleen werken met Postgres "
|
|
||||||
"databases."
|
|
||||||
|
|
||||||
#: .\cookbook\templates\url_import.html:5
|
#: .\cookbook\templates\url_import.html:5
|
||||||
msgid "URL Import"
|
msgid "URL Import"
|
||||||
@ -1763,11 +1657,9 @@ msgstr "Informatie"
|
|||||||
#: .\cookbook\templates\url_import.html:235
|
#: .\cookbook\templates\url_import.html:235
|
||||||
msgid ""
|
msgid ""
|
||||||
" Only websites containing ld+json or microdata information can currently\n"
|
" Only websites containing ld+json or microdata information can currently\n"
|
||||||
" be imported. Most big recipe pages "
|
" be imported. Most big recipe pages support this. If you site cannot be imported but\n"
|
||||||
"support this. If you site cannot be imported but\n"
|
|
||||||
" you think\n"
|
" you think\n"
|
||||||
" it probably has some kind of structured "
|
" it probably has some kind of structured data feel free to post an example in the\n"
|
||||||
"data feel free to post an example in the\n"
|
|
||||||
" github issues."
|
" github issues."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Alleen websites die Id+json of microdata informatie bevatten kunnen op dit "
|
"Alleen websites die Id+json of microdata informatie bevatten kunnen op dit "
|
||||||
@ -1798,7 +1690,7 @@ msgstr "Voorkeur voor gebruiker bestaat al"
|
|||||||
|
|
||||||
#: .\cookbook\views\api.py:416 .\cookbook\views\views.py:265
|
#: .\cookbook\views\api.py:416 .\cookbook\views\views.py:265
|
||||||
msgid "This feature is not available in the demo version!"
|
msgid "This feature is not available in the demo version!"
|
||||||
msgstr ""
|
msgstr "Deze optie is niet beschikbaar in de demo versie!"
|
||||||
|
|
||||||
#: .\cookbook\views\api.py:439
|
#: .\cookbook\views\api.py:439
|
||||||
msgid "Sync successful!"
|
msgid "Sync successful!"
|
||||||
@ -1888,7 +1780,7 @@ msgstr "Eenheden samengevoegd!"
|
|||||||
|
|
||||||
#: .\cookbook\views\edit.py:295 .\cookbook\views\edit.py:317
|
#: .\cookbook\views\edit.py:295 .\cookbook\views\edit.py:317
|
||||||
msgid "Cannot merge with the same object!"
|
msgid "Cannot merge with the same object!"
|
||||||
msgstr ""
|
msgstr "Kan niet met hetzelfde object samenvoegen!"
|
||||||
|
|
||||||
#: .\cookbook\views\edit.py:311
|
#: .\cookbook\views\edit.py:311
|
||||||
msgid "Foods merged!"
|
msgid "Foods merged!"
|
||||||
@ -1896,11 +1788,11 @@ msgstr "Ingrediënten samengevoegd!"
|
|||||||
|
|
||||||
#: .\cookbook\views\import_export.py:42
|
#: .\cookbook\views\import_export.py:42
|
||||||
msgid "Importing is not implemented for this provider"
|
msgid "Importing is not implemented for this provider"
|
||||||
msgstr ""
|
msgstr "Importeren is voor deze provider niet geïmplementeerd"
|
||||||
|
|
||||||
#: .\cookbook\views\import_export.py:58
|
#: .\cookbook\views\import_export.py:58
|
||||||
msgid "Exporting is not implemented for this provider"
|
msgid "Exporting is not implemented for this provider"
|
||||||
msgstr ""
|
msgstr "Exporteren is voor deze provider niet geïmplementeerd"
|
||||||
|
|
||||||
#: .\cookbook\views\lists.py:42
|
#: .\cookbook\views\lists.py:42
|
||||||
msgid "Import Log"
|
msgid "Import Log"
|
||||||
@ -1932,7 +1824,7 @@ msgstr "Opmerking opgeslagen!"
|
|||||||
|
|
||||||
#: .\cookbook\views\views.py:152
|
#: .\cookbook\views\views.py:152
|
||||||
msgid "This recipe is already linked to the book!"
|
msgid "This recipe is already linked to the book!"
|
||||||
msgstr ""
|
msgstr "Dit recept is al aan het boek gekoppeld!"
|
||||||
|
|
||||||
#: .\cookbook\views\views.py:158
|
#: .\cookbook\views\views.py:158
|
||||||
msgid "Bookmark saved!"
|
msgid "Bookmark saved!"
|
||||||
@ -1964,150 +1856,3 @@ msgstr "Onjuiste uitnodigingslink opgegeven!"
|
|||||||
#: .\cookbook\views\views.py:470
|
#: .\cookbook\views\views.py:470
|
||||||
msgid "Invite Link not valid or already used!"
|
msgid "Invite Link not valid or already used!"
|
||||||
msgstr "De uitnodigingslink is niet valide of al gebruikt!"
|
msgstr "De uitnodigingslink is niet valide of al gebruikt!"
|
||||||
|
|
||||||
#~ msgid "Export Base64 encoded image?"
|
|
||||||
#~ msgstr "Base64-gecodeerde afbeelding exporteren?"
|
|
||||||
|
|
||||||
#~ msgid "Download export directly or show on page?"
|
|
||||||
#~ msgstr "De export direct downloaden of op de pagina weergeven?"
|
|
||||||
|
|
||||||
#~ msgid "Simply paste a JSON export into this textarea and click import."
|
|
||||||
#~ msgstr "Plak een JSON export in dit tekstveld en klik op importeren."
|
|
||||||
|
|
||||||
#~ msgid "Scaling factor for recipe."
|
|
||||||
#~ msgstr "Schaalfactor voor recept."
|
|
||||||
|
|
||||||
#~ msgid "Exported Recipe"
|
|
||||||
#~ msgstr "Geëxporteerd recept"
|
|
||||||
|
|
||||||
#~ msgid "Copy to clipboard"
|
|
||||||
#~ msgstr "Naar het klembord kopiëren"
|
|
||||||
|
|
||||||
#~ msgid "Copied!"
|
|
||||||
#~ msgstr "Gekopieerd!"
|
|
||||||
|
|
||||||
#~ msgid "Copy list to clipboard"
|
|
||||||
#~ msgstr "Lijst naar het klembord kopiëren"
|
|
||||||
|
|
||||||
#~ msgid "Error"
|
|
||||||
#~ msgstr "Error"
|
|
||||||
|
|
||||||
#~ msgid "There was an error loading the recipe!"
|
|
||||||
#~ msgstr "Er is een fout opgetreden bij het laden van het recept!"
|
|
||||||
|
|
||||||
#~ msgid "Updated"
|
|
||||||
#~ msgstr "Geüpdatet"
|
|
||||||
|
|
||||||
#~ msgid "Changes saved successfully!"
|
|
||||||
#~ msgstr "Wijzigingen succesvol opgeslagen!"
|
|
||||||
|
|
||||||
#~ msgid "There was an error updating the recipe!"
|
|
||||||
#~ msgstr "Er is een fout opgetreden bij het updaten van het recept!"
|
|
||||||
|
|
||||||
#~ msgid "Are you sure that you want to delete this ingredient?"
|
|
||||||
#~ msgstr "Weet je zeker dat je dit ingrediënt wil verwijderen?"
|
|
||||||
|
|
||||||
#~ msgid "Are you sure that you want to delete this step?"
|
|
||||||
#~ msgstr "Weet je zeker dat je deze stap wil verwijderen?"
|
|
||||||
|
|
||||||
#~ msgid "There was an error loading a resource!"
|
|
||||||
#~ msgstr "Er is een fout opgetreden bij het laden van een hulpbron!"
|
|
||||||
|
|
||||||
#~ msgid "Recipe Multiplier"
|
|
||||||
#~ msgstr "Recept vermenigvuldiger"
|
|
||||||
|
|
||||||
#~ msgid ""
|
|
||||||
#~ "When deleting a meal type all entries using that type will be deleted as "
|
|
||||||
#~ "well. Deletion will apply when configuration is saved. Do you want to "
|
|
||||||
#~ "proceed?"
|
|
||||||
#~ msgstr ""
|
|
||||||
#~ "Bij het verwijderen van een maaltijdsoort worden alle inzendingen die de "
|
|
||||||
#~ "maaltijdsoort gebruikt verwijderd. Verwijdering vindt plaats wanneer de "
|
|
||||||
#~ "configuratie opgeslagen wordt. Wil je doorgaan?"
|
|
||||||
|
|
||||||
#~ msgid "Add to Book"
|
|
||||||
#~ msgstr "Aan Boek toevoegen"
|
|
||||||
|
|
||||||
#~ msgid "Add to Plan"
|
|
||||||
#~ msgstr "Aan Plan toevoegen"
|
|
||||||
|
|
||||||
#~ msgid "Print"
|
|
||||||
#~ msgstr "Printen"
|
|
||||||
|
|
||||||
#~ msgid "Share"
|
|
||||||
#~ msgstr "Deel"
|
|
||||||
|
|
||||||
#~ msgid "in"
|
|
||||||
#~ msgstr "binnen"
|
|
||||||
|
|
||||||
#~ msgid "Preparation time ~"
|
|
||||||
#~ msgstr "Bereidingstijd"
|
|
||||||
|
|
||||||
#~ msgid "Minutes"
|
|
||||||
#~ msgstr "Minuten"
|
|
||||||
|
|
||||||
#~ msgid "View external recipe"
|
|
||||||
#~ msgstr "Extern recept bekijken"
|
|
||||||
|
|
||||||
#~ msgid "External recipe image"
|
|
||||||
#~ msgstr "Externe recept afbeelding"
|
|
||||||
|
|
||||||
#~ msgid "External recipe"
|
|
||||||
#~ msgstr "Extern recept"
|
|
||||||
|
|
||||||
#~ msgid ""
|
|
||||||
#~ "\n"
|
|
||||||
#~ " This is an external recipe, which "
|
|
||||||
#~ "means you can only view it by opening the link\n"
|
|
||||||
#~ " above.\n"
|
|
||||||
#~ " You can convert this recipe to a "
|
|
||||||
#~ "fancy recipe by pressing the convert button. The\n"
|
|
||||||
#~ " original\n"
|
|
||||||
#~ " file\n"
|
|
||||||
#~ " will still be accessible.\n"
|
|
||||||
#~ " "
|
|
||||||
#~ msgstr ""
|
|
||||||
#~ "\n"
|
|
||||||
#~ "Dit is een extern recept, dat betekent dat je het dient te openen met de "
|
|
||||||
#~ "bovenstaande link.\n"
|
|
||||||
#~ "Je kan dit recept naar een flitsend recept omzetten door op de converteer "
|
|
||||||
#~ "knop te klikken.\n"
|
|
||||||
#~ "Het originele bestand blijft beschikbaar."
|
|
||||||
|
|
||||||
#~ msgid "Convert now!"
|
|
||||||
#~ msgstr "Nu converteren"
|
|
||||||
|
|
||||||
#~ msgid "Your username and password didn't match. Please try again."
|
|
||||||
#~ msgstr ""
|
|
||||||
#~ "Je gebruikersnaam en wachtwoord komen niet overeen. Probeer het opnieuw."
|
|
||||||
|
|
||||||
#~ msgid "There was an error updating a resource!"
|
|
||||||
#~ msgstr "Er is een fout opgetreden bij het updaten van een hulpbron!"
|
|
||||||
|
|
||||||
#~ msgid "Object created successfully!"
|
|
||||||
#~ msgstr "Object succesvol aangemaakt!"
|
|
||||||
|
|
||||||
#~ msgid "Please enter a valid food"
|
|
||||||
#~ msgstr "Geef een geldig ingrediënt op"
|
|
||||||
|
|
||||||
#~ msgid "Already importing the selected recipe, please wait!"
|
|
||||||
#~ msgstr "Het geselecteerde recept wordt geïmporteerd, even geduld!"
|
|
||||||
|
|
||||||
#~ msgid "An error occurred while trying to import this recipe!"
|
|
||||||
#~ msgstr "Er is een error opgetreden bij het importeren van dit recept!"
|
|
||||||
|
|
||||||
#~ msgid "Recipe imported successfully!"
|
|
||||||
#~ msgstr "Recept succesvol geïmporteerd!"
|
|
||||||
|
|
||||||
#~ msgid "Something went wrong during the import!"
|
|
||||||
#~ msgstr "Er is iets misgegaan tijdens het importeren!"
|
|
||||||
|
|
||||||
#~ msgid "Could not parse the supplied JSON!"
|
|
||||||
#~ msgstr "Er zit een fout in de opgegeven JSON!"
|
|
||||||
|
|
||||||
#~ msgid ""
|
|
||||||
#~ "External recipes cannot be exported, please share the file directly or "
|
|
||||||
#~ "select an internal recipe."
|
|
||||||
#~ msgstr ""
|
|
||||||
#~ "Het is niet mogelijk om externe recepten te exporteren. Deel het bestand "
|
|
||||||
#~ "zelf of selecteer een intern recept."
|
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
# Generated by Django 3.0.2 on 2020-01-30 09:59
|
# Generated by Django 3.0.2 on 2020-01-30 09:59
|
||||||
|
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
|
||||||
def migrate_ingredient_units(apps, schema_editor):
|
def migrate_ingredient_units(apps, schema_editor):
|
||||||
|
with scopes_disabled():
|
||||||
Unit = apps.get_model('cookbook', 'Unit')
|
Unit = apps.get_model('cookbook', 'Unit')
|
||||||
RecipeIngredients = apps.get_model('cookbook', 'RecipeIngredients')
|
RecipeIngredients = apps.get_model('cookbook', 'RecipeIngredients')
|
||||||
|
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
# Generated by Django 3.0.2 on 2020-02-16 22:09
|
# Generated by Django 3.0.2 on 2020-02-16 22:09
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
|
||||||
def migrate_ingredients(apps, schema_editor):
|
def migrate_ingredients(apps, schema_editor):
|
||||||
|
with scopes_disabled():
|
||||||
Ingredient = apps.get_model('cookbook', 'Ingredient')
|
Ingredient = apps.get_model('cookbook', 'Ingredient')
|
||||||
RecipeIngredient = apps.get_model('cookbook', 'RecipeIngredient')
|
RecipeIngredient = apps.get_model('cookbook', 'RecipeIngredient')
|
||||||
|
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
# Generated by Django 3.0.5 on 2020-04-26 14:14
|
# Generated by Django 3.0.5 on 2020-04-26 14:14
|
||||||
|
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
|
||||||
def apply_migration(apps, schema_editor):
|
def apply_migration(apps, schema_editor):
|
||||||
|
with scopes_disabled():
|
||||||
Group = apps.get_model('auth', 'Group')
|
Group = apps.get_model('auth', 'Group')
|
||||||
Group.objects.bulk_create([
|
Group.objects.bulk_create([
|
||||||
Group(name=u'guest'),
|
Group(name=u'guest'),
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
# Generated by Django 3.0.5 on 2020-04-27 16:00
|
# Generated by Django 3.0.5 on 2020-04-27 16:00
|
||||||
|
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
|
||||||
def apply_migration(apps, schema_editor):
|
def apply_migration(apps, schema_editor):
|
||||||
|
with scopes_disabled():
|
||||||
Group = apps.get_model('auth', 'Group')
|
Group = apps.get_model('auth', 'Group')
|
||||||
User = apps.get_model('auth', 'User')
|
User = apps.get_model('auth', 'User')
|
||||||
for u in User.objects.all():
|
for u in User.objects.all():
|
||||||
|
@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
|
||||||
def migrate_meal_types(apps, schema_editor):
|
def migrate_meal_types(apps, schema_editor):
|
||||||
|
with scopes_disabled():
|
||||||
MealPlan = apps.get_model('cookbook', 'MealPlan')
|
MealPlan = apps.get_model('cookbook', 'MealPlan')
|
||||||
MealType = apps.get_model('cookbook', 'MealType')
|
MealType = apps.get_model('cookbook', 'MealType')
|
||||||
|
|
||||||
|
@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
|
||||||
def migrate_meal_types(apps, schema_editor):
|
def migrate_meal_types(apps, schema_editor):
|
||||||
|
with scopes_disabled():
|
||||||
MealPlan = apps.get_model('cookbook', 'MealPlan')
|
MealPlan = apps.get_model('cookbook', 'MealPlan')
|
||||||
MealType = apps.get_model('cookbook', 'MealType')
|
MealType = apps.get_model('cookbook', 'MealType')
|
||||||
User = apps.get_model('auth', 'User')
|
User = apps.get_model('auth', 'User')
|
||||||
|
@ -3,8 +3,11 @@
|
|||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
|
||||||
def invalidate_shares(apps, schema_editor):
|
def invalidate_shares(apps, schema_editor):
|
||||||
|
with scopes_disabled():
|
||||||
ShareLink = apps.get_model('cookbook', 'ShareLink')
|
ShareLink = apps.get_model('cookbook', 'ShareLink')
|
||||||
|
|
||||||
ShareLink.objects.all().delete()
|
ShareLink.objects.all().delete()
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
# Generated by Django 3.0.7 on 2020-06-25 19:37
|
# Generated by Django 3.0.7 on 2020-06-25 19:37
|
||||||
|
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
|
||||||
def migrate_ingredients(apps, schema_editor):
|
def migrate_ingredients(apps, schema_editor):
|
||||||
|
with scopes_disabled():
|
||||||
Recipe = apps.get_model('cookbook', 'Recipe')
|
Recipe = apps.get_model('cookbook', 'Recipe')
|
||||||
Ingredient = apps.get_model('cookbook', 'Ingredient')
|
Ingredient = apps.get_model('cookbook', 'Ingredient')
|
||||||
|
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
# Generated by Django 3.0.7 on 2020-06-25 20:19
|
# Generated by Django 3.0.7 on 2020-06-25 20:19
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
|
||||||
def create_default_step(apps, schema_editor):
|
def create_default_step(apps, schema_editor):
|
||||||
|
with scopes_disabled():
|
||||||
Recipe = apps.get_model('cookbook', 'Recipe')
|
Recipe = apps.get_model('cookbook', 'Recipe')
|
||||||
Step = apps.get_model('cookbook', 'Step')
|
Step = apps.get_model('cookbook', 'Step')
|
||||||
|
|
||||||
|
@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
|
||||||
def convert_old_specials(apps, schema_editor):
|
def convert_old_specials(apps, schema_editor):
|
||||||
|
with scopes_disabled():
|
||||||
Ingredient = apps.get_model('cookbook', 'Ingredient')
|
Ingredient = apps.get_model('cookbook', 'Ingredient')
|
||||||
Food = apps.get_model('cookbook', 'Food')
|
Food = apps.get_model('cookbook', 'Food')
|
||||||
Unit = apps.get_model('cookbook', 'Unit')
|
Unit = apps.get_model('cookbook', 'Unit')
|
||||||
|
146
cookbook/migrations/0108_auto_20210219_1410.py
Normal file
146
cookbook/migrations/0108_auto_20210219_1410.py
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
# Generated by Django 3.1.6 on 2021-02-19 13:10
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('cookbook', '0107_auto_20210128_1535'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='cooklog',
|
||||||
|
name='space',
|
||||||
|
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='food',
|
||||||
|
name='space',
|
||||||
|
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='invitelink',
|
||||||
|
name='space',
|
||||||
|
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='keyword',
|
||||||
|
name='space',
|
||||||
|
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='mealplan',
|
||||||
|
name='space',
|
||||||
|
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='mealtype',
|
||||||
|
name='space',
|
||||||
|
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='recipe',
|
||||||
|
name='space',
|
||||||
|
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='recipebook',
|
||||||
|
name='space',
|
||||||
|
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='recipebookentry',
|
||||||
|
name='space',
|
||||||
|
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='recipeimport',
|
||||||
|
name='space',
|
||||||
|
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sharelink',
|
||||||
|
name='space',
|
||||||
|
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='shoppinglist',
|
||||||
|
name='space',
|
||||||
|
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='shoppinglistentry',
|
||||||
|
name='space',
|
||||||
|
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='shoppinglistrecipe',
|
||||||
|
name='space',
|
||||||
|
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='storage',
|
||||||
|
name='space',
|
||||||
|
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='supermarket',
|
||||||
|
name='space',
|
||||||
|
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='supermarketcategory',
|
||||||
|
name='space',
|
||||||
|
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sync',
|
||||||
|
name='space',
|
||||||
|
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='synclog',
|
||||||
|
name='space',
|
||||||
|
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='unit',
|
||||||
|
name='space',
|
||||||
|
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='userpreference',
|
||||||
|
name='space',
|
||||||
|
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='viewlog',
|
||||||
|
name='space',
|
||||||
|
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
63
cookbook/migrations/0109_auto_20210221_1204.py
Normal file
63
cookbook/migrations/0109_auto_20210221_1204.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# Generated by Django 3.1.6 on 2021-02-21 11:04
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('cookbook', '0108_auto_20210219_1410'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='recipebookentry',
|
||||||
|
name='space',
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='food',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=128, validators=[django.core.validators.MinLengthValidator(1)]),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='keyword',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=64),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='supermarket',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=128, validators=[django.core.validators.MinLengthValidator(1)]),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='supermarketcategory',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=128, validators=[django.core.validators.MinLengthValidator(1)]),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='unit',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=128, validators=[django.core.validators.MinLengthValidator(1)]),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='food',
|
||||||
|
unique_together={('space', 'name')},
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='keyword',
|
||||||
|
unique_together={('space', 'name')},
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='supermarket',
|
||||||
|
unique_together={('space', 'name')},
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='supermarketcategory',
|
||||||
|
unique_together={('space', 'name')},
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='unit',
|
||||||
|
unique_together={('space', 'name')},
|
||||||
|
),
|
||||||
|
]
|
19
cookbook/migrations/0110_auto_20210221_1406.py
Normal file
19
cookbook/migrations/0110_auto_20210221_1406.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 3.1.6 on 2021-02-21 13:06
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('cookbook', '0109_auto_20210221_1204'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='userpreference',
|
||||||
|
name='space',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||||
|
),
|
||||||
|
]
|
32
cookbook/migrations/0111_space_created_by.py
Normal file
32
cookbook/migrations/0111_space_created_by.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# Generated by Django 3.1.6 on 2021-02-21 13:19
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
|
||||||
|
def set_default_owner(apps, schema_editor):
|
||||||
|
Space = apps.get_model('cookbook', 'Space')
|
||||||
|
User = apps.get_model('auth', 'user')
|
||||||
|
|
||||||
|
with scopes_disabled():
|
||||||
|
for x in Space.objects.all():
|
||||||
|
x.created_by = User.objects.filter(is_superuser=True).first()
|
||||||
|
x.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('cookbook', '0110_auto_20210221_1406'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='space',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.RunPython(set_default_owner),
|
||||||
|
]
|
17
cookbook/migrations/0112_remove_synclog_space.py
Normal file
17
cookbook/migrations/0112_remove_synclog_space.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-03-16 23:21
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('cookbook', '0111_space_created_by'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='synclog',
|
||||||
|
name='space',
|
||||||
|
),
|
||||||
|
]
|
21
cookbook/migrations/0113_auto_20210317_2017.py
Normal file
21
cookbook/migrations/0113_auto_20210317_2017.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-03-17 19:17
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('cookbook', '0112_remove_synclog_space'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='shoppinglistentry',
|
||||||
|
name='space',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='shoppinglistrecipe',
|
||||||
|
name='space',
|
||||||
|
),
|
||||||
|
]
|
31
cookbook/migrations/0114_importlog.py
Normal file
31
cookbook/migrations/0114_importlog.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Generated by Django 3.1.7 on 2021-03-18 17:23
|
||||||
|
|
||||||
|
import cookbook.models
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('cookbook', '0113_auto_20210317_2017'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ImportLog',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('type', models.CharField(max_length=32)),
|
||||||
|
('running', models.BooleanField(default=True)),
|
||||||
|
('msg', models.TextField(default='')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
('keyword', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='cookbook.keyword')),
|
||||||
|
('space', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space')),
|
||||||
|
],
|
||||||
|
bases=(models.Model, cookbook.models.PermissionModelMixin),
|
||||||
|
),
|
||||||
|
]
|
@ -9,7 +9,7 @@ from django.core.validators import MinLengthValidator
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django_random_queryset import RandomManager
|
from django_scopes import ScopedManager
|
||||||
|
|
||||||
from recipes.settings import (COMMENT_PREF_DEFAULT, FRACTION_PREF_DEFAULT,
|
from recipes.settings import (COMMENT_PREF_DEFAULT, FRACTION_PREF_DEFAULT,
|
||||||
STICKY_NAV_PREF_DEFAULT)
|
STICKY_NAV_PREF_DEFAULT)
|
||||||
@ -29,12 +29,44 @@ def get_model_name(model):
|
|||||||
return ('_'.join(re.findall('[A-Z][^A-Z]*', model.__name__))).lower()
|
return ('_'.join(re.findall('[A-Z][^A-Z]*', model.__name__))).lower()
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionModelMixin:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_space_key():
|
||||||
|
return ('space',)
|
||||||
|
|
||||||
|
def get_space_kwarg(self):
|
||||||
|
return '__'.join(self.get_space_key())
|
||||||
|
|
||||||
|
def get_owner(self):
|
||||||
|
if getattr(self, 'created_by', None):
|
||||||
|
return self.created_by
|
||||||
|
if getattr(self, 'user', None):
|
||||||
|
return self.user
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_shared(self):
|
||||||
|
if getattr(self, 'shared', None):
|
||||||
|
return self.shared.all()
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_space(self):
|
||||||
|
p = '.'.join(self.get_space_key())
|
||||||
|
if getattr(self, p, None):
|
||||||
|
return getattr(self, p)
|
||||||
|
raise NotImplementedError('get space for method not implemented and standard fields not available')
|
||||||
|
|
||||||
|
|
||||||
class Space(models.Model):
|
class Space(models.Model):
|
||||||
name = models.CharField(max_length=128, default='Default')
|
name = models.CharField(max_length=128, default='Default')
|
||||||
|
created_by = models.ForeignKey(User, on_delete=models.PROTECT, null=True)
|
||||||
message = models.CharField(max_length=512, default='', blank=True)
|
message = models.CharField(max_length=512, default='', blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
class UserPreference(models.Model):
|
|
||||||
|
class UserPreference(models.Model, PermissionModelMixin):
|
||||||
# Themes
|
# Themes
|
||||||
BOOTSTRAP = 'BOOTSTRAP'
|
BOOTSTRAP = 'BOOTSTRAP'
|
||||||
DARKLY = 'DARKLY'
|
DARKLY = 'DARKLY'
|
||||||
@ -107,11 +139,14 @@ class UserPreference(models.Model):
|
|||||||
shopping_auto_sync = models.IntegerField(default=5)
|
shopping_auto_sync = models.IntegerField(default=5)
|
||||||
sticky_navbar = models.BooleanField(default=STICKY_NAV_PREF_DEFAULT)
|
sticky_navbar = models.BooleanField(default=STICKY_NAV_PREF_DEFAULT)
|
||||||
|
|
||||||
|
space = models.ForeignKey(Space, on_delete=models.CASCADE, null=True)
|
||||||
|
objects = ScopedManager(space='space')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.user)
|
return str(self.user)
|
||||||
|
|
||||||
|
|
||||||
class Storage(models.Model):
|
class Storage(models.Model, PermissionModelMixin):
|
||||||
DROPBOX = 'DB'
|
DROPBOX = 'DB'
|
||||||
NEXTCLOUD = 'NEXTCLOUD'
|
NEXTCLOUD = 'NEXTCLOUD'
|
||||||
LOCAL = 'LOCAL'
|
LOCAL = 'LOCAL'
|
||||||
@ -128,11 +163,14 @@ class Storage(models.Model):
|
|||||||
path = models.CharField(blank=True, default='', max_length=256)
|
path = models.CharField(blank=True, default='', max_length=256)
|
||||||
created_by = models.ForeignKey(User, on_delete=models.PROTECT)
|
created_by = models.ForeignKey(User, on_delete=models.PROTECT)
|
||||||
|
|
||||||
|
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||||
|
objects = ScopedManager(space='space')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
class Sync(models.Model):
|
class Sync(models.Model, PermissionModelMixin):
|
||||||
storage = models.ForeignKey(Storage, on_delete=models.PROTECT)
|
storage = models.ForeignKey(Storage, on_delete=models.PROTECT)
|
||||||
path = models.CharField(max_length=512, default="")
|
path = models.CharField(max_length=512, default="")
|
||||||
active = models.BooleanField(default=True)
|
active = models.BooleanField(default=True)
|
||||||
@ -140,92 +178,138 @@ class Sync(models.Model):
|
|||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||||
|
objects = ScopedManager(space='space')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.path
|
return self.path
|
||||||
|
|
||||||
|
|
||||||
class SupermarketCategory(models.Model):
|
class SupermarketCategory(models.Model, PermissionModelMixin):
|
||||||
name = models.CharField(unique=True, max_length=128, validators=[MinLengthValidator(1)])
|
name = models.CharField(max_length=128, validators=[MinLengthValidator(1)])
|
||||||
description = models.TextField(blank=True, null=True)
|
description = models.TextField(blank=True, null=True)
|
||||||
|
|
||||||
|
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||||
|
objects = ScopedManager(space='space')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = (('space', 'name'),)
|
||||||
|
|
||||||
class Supermarket(models.Model):
|
|
||||||
name = models.CharField(unique=True, max_length=128, validators=[MinLengthValidator(1)])
|
class Supermarket(models.Model, PermissionModelMixin):
|
||||||
|
name = models.CharField(max_length=128, validators=[MinLengthValidator(1)])
|
||||||
description = models.TextField(blank=True, null=True)
|
description = models.TextField(blank=True, null=True)
|
||||||
categories = models.ManyToManyField(SupermarketCategory, through='SupermarketCategoryRelation')
|
categories = models.ManyToManyField(SupermarketCategory, through='SupermarketCategoryRelation')
|
||||||
|
|
||||||
|
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||||
|
objects = ScopedManager(space='space')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = (('space', 'name'),)
|
||||||
|
|
||||||
class SupermarketCategoryRelation(models.Model):
|
|
||||||
|
class SupermarketCategoryRelation(models.Model, PermissionModelMixin):
|
||||||
supermarket = models.ForeignKey(Supermarket, on_delete=models.CASCADE, related_name='category_to_supermarket')
|
supermarket = models.ForeignKey(Supermarket, on_delete=models.CASCADE, related_name='category_to_supermarket')
|
||||||
category = models.ForeignKey(SupermarketCategory, on_delete=models.CASCADE, related_name='category_to_supermarket')
|
category = models.ForeignKey(SupermarketCategory, on_delete=models.CASCADE, related_name='category_to_supermarket')
|
||||||
order = models.IntegerField(default=0)
|
order = models.IntegerField(default=0)
|
||||||
|
|
||||||
|
objects = ScopedManager(space='supermarket__space')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_space_key():
|
||||||
|
return 'supermarket', 'space'
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('order',)
|
ordering = ('order',)
|
||||||
|
|
||||||
|
|
||||||
class SyncLog(models.Model):
|
class SyncLog(models.Model, PermissionModelMixin):
|
||||||
sync = models.ForeignKey(Sync, on_delete=models.CASCADE)
|
sync = models.ForeignKey(Sync, on_delete=models.CASCADE)
|
||||||
status = models.CharField(max_length=32)
|
status = models.CharField(max_length=32)
|
||||||
msg = models.TextField(default="")
|
msg = models.TextField(default="")
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
objects = ScopedManager(space='sync__space')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.created_at}:{self.sync} - {self.status}"
|
return f"{self.created_at}:{self.sync} - {self.status}"
|
||||||
|
|
||||||
|
|
||||||
class Keyword(models.Model):
|
class Keyword(models.Model, PermissionModelMixin):
|
||||||
name = models.CharField(max_length=64, unique=True)
|
name = models.CharField(max_length=64)
|
||||||
icon = models.CharField(max_length=16, blank=True, null=True)
|
icon = models.CharField(max_length=16, blank=True, null=True)
|
||||||
description = models.TextField(default="", blank=True)
|
description = models.TextField(default="", blank=True)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||||
|
objects = ScopedManager(space='space')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.icon:
|
if self.icon:
|
||||||
return f"{self.icon} {self.name}"
|
return f"{self.icon} {self.name}"
|
||||||
else:
|
else:
|
||||||
return f"{self.name}"
|
return f"{self.name}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = (('space', 'name'),)
|
||||||
|
|
||||||
class Unit(models.Model):
|
|
||||||
name = models.CharField(unique=True, max_length=128, validators=[MinLengthValidator(1)])
|
class Unit(models.Model, PermissionModelMixin):
|
||||||
|
name = models.CharField(max_length=128, validators=[MinLengthValidator(1)])
|
||||||
description = models.TextField(blank=True, null=True)
|
description = models.TextField(blank=True, null=True)
|
||||||
|
|
||||||
|
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||||
|
objects = ScopedManager(space='space')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = (('space', 'name'),)
|
||||||
|
|
||||||
class Food(models.Model):
|
|
||||||
name = models.CharField(unique=True, max_length=128, validators=[MinLengthValidator(1)])
|
class Food(models.Model, PermissionModelMixin):
|
||||||
|
name = models.CharField(max_length=128, validators=[MinLengthValidator(1)])
|
||||||
recipe = models.ForeignKey('Recipe', null=True, blank=True, on_delete=models.SET_NULL)
|
recipe = models.ForeignKey('Recipe', null=True, blank=True, on_delete=models.SET_NULL)
|
||||||
supermarket_category = models.ForeignKey(SupermarketCategory, null=True, blank=True, on_delete=models.SET_NULL)
|
supermarket_category = models.ForeignKey(SupermarketCategory, null=True, blank=True, on_delete=models.SET_NULL)
|
||||||
ignore_shopping = models.BooleanField(default=False)
|
ignore_shopping = models.BooleanField(default=False)
|
||||||
description = models.TextField(default='', blank=True)
|
description = models.TextField(default='', blank=True)
|
||||||
|
|
||||||
|
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||||
|
objects = ScopedManager(space='space')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = (('space', 'name'),)
|
||||||
|
|
||||||
class Ingredient(models.Model):
|
|
||||||
food = models.ForeignKey(
|
class Ingredient(models.Model, PermissionModelMixin):
|
||||||
Food, on_delete=models.PROTECT, null=True, blank=True
|
food = models.ForeignKey(Food, on_delete=models.PROTECT, null=True, blank=True)
|
||||||
)
|
unit = models.ForeignKey(Unit, on_delete=models.PROTECT, null=True, blank=True)
|
||||||
unit = models.ForeignKey(
|
|
||||||
Unit, on_delete=models.PROTECT, null=True, blank=True
|
|
||||||
)
|
|
||||||
amount = models.DecimalField(default=0, decimal_places=16, max_digits=32)
|
amount = models.DecimalField(default=0, decimal_places=16, max_digits=32)
|
||||||
note = models.CharField(max_length=256, null=True, blank=True)
|
note = models.CharField(max_length=256, null=True, blank=True)
|
||||||
is_header = models.BooleanField(default=False)
|
is_header = models.BooleanField(default=False)
|
||||||
no_amount = models.BooleanField(default=False)
|
no_amount = models.BooleanField(default=False)
|
||||||
order = models.IntegerField(default=0)
|
order = models.IntegerField(default=0)
|
||||||
|
|
||||||
|
objects = ScopedManager(space='step__recipe__space')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_space_key():
|
||||||
|
return 'step', 'recipe', 'space'
|
||||||
|
|
||||||
|
def get_space(self):
|
||||||
|
return self.step_set.first().recipe_set.first().space
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.amount) + ' ' + str(self.unit) + ' ' + str(self.food)
|
return str(self.amount) + ' ' + str(self.unit) + ' ' + str(self.food)
|
||||||
|
|
||||||
@ -233,7 +317,7 @@ class Ingredient(models.Model):
|
|||||||
ordering = ['order', 'pk']
|
ordering = ['order', 'pk']
|
||||||
|
|
||||||
|
|
||||||
class Step(models.Model):
|
class Step(models.Model, PermissionModelMixin):
|
||||||
TEXT = 'TEXT'
|
TEXT = 'TEXT'
|
||||||
TIME = 'TIME'
|
TIME = 'TIME'
|
||||||
|
|
||||||
@ -249,6 +333,15 @@ class Step(models.Model):
|
|||||||
order = models.IntegerField(default=0)
|
order = models.IntegerField(default=0)
|
||||||
show_as_header = models.BooleanField(default=True)
|
show_as_header = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
objects = ScopedManager(space='recipe__space')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_space_key():
|
||||||
|
return 'recipe', 'space'
|
||||||
|
|
||||||
|
def get_space(self):
|
||||||
|
return self.recipe_set.first().space
|
||||||
|
|
||||||
def get_instruction_render(self):
|
def get_instruction_render(self):
|
||||||
from cookbook.helper.template_helper import render_instructions
|
from cookbook.helper.template_helper import render_instructions
|
||||||
return render_instructions(self)
|
return render_instructions(self)
|
||||||
@ -257,7 +350,7 @@ class Step(models.Model):
|
|||||||
ordering = ['order', 'pk']
|
ordering = ['order', 'pk']
|
||||||
|
|
||||||
|
|
||||||
class NutritionInformation(models.Model):
|
class NutritionInformation(models.Model, PermissionModelMixin):
|
||||||
fats = models.DecimalField(default=0, decimal_places=16, max_digits=32)
|
fats = models.DecimalField(default=0, decimal_places=16, max_digits=32)
|
||||||
carbohydrates = models.DecimalField(
|
carbohydrates = models.DecimalField(
|
||||||
default=0, decimal_places=16, max_digits=32
|
default=0, decimal_places=16, max_digits=32
|
||||||
@ -268,11 +361,20 @@ class NutritionInformation(models.Model):
|
|||||||
max_length=512, default="", null=True, blank=True
|
max_length=512, default="", null=True, blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
objects = ScopedManager(space='recipe__space')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_space_key():
|
||||||
|
return 'recipe', 'space'
|
||||||
|
|
||||||
|
def get_space(self):
|
||||||
|
return self.recipe_set.first().space
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return 'Nutrition'
|
return 'Nutrition'
|
||||||
|
|
||||||
|
|
||||||
class Recipe(models.Model):
|
class Recipe(models.Model, PermissionModelMixin):
|
||||||
name = models.CharField(max_length=128)
|
name = models.CharField(max_length=128)
|
||||||
description = models.CharField(max_length=512, blank=True, null=True)
|
description = models.CharField(max_length=512, blank=True, null=True)
|
||||||
servings = models.IntegerField(default=1)
|
servings = models.IntegerField(default=1)
|
||||||
@ -297,51 +399,68 @@ class Recipe(models.Model):
|
|||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
objects = RandomManager()
|
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||||
|
objects = ScopedManager(space='space')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
class Comment(models.Model):
|
class Comment(models.Model, PermissionModelMixin):
|
||||||
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
|
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
|
||||||
text = models.TextField()
|
text = models.TextField()
|
||||||
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
objects = ScopedManager(space='recipe__space')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_space_key():
|
||||||
|
return 'recipe', 'space'
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.text
|
return self.text
|
||||||
|
|
||||||
|
|
||||||
class RecipeImport(models.Model):
|
class RecipeImport(models.Model, PermissionModelMixin):
|
||||||
name = models.CharField(max_length=128)
|
name = models.CharField(max_length=128)
|
||||||
storage = models.ForeignKey(Storage, on_delete=models.PROTECT)
|
storage = models.ForeignKey(Storage, on_delete=models.PROTECT)
|
||||||
file_uid = models.CharField(max_length=256, default="")
|
file_uid = models.CharField(max_length=256, default="")
|
||||||
file_path = models.CharField(max_length=512, default="")
|
file_path = models.CharField(max_length=512, default="")
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||||
|
objects = ScopedManager(space='space')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
class RecipeBook(models.Model):
|
class RecipeBook(models.Model, PermissionModelMixin):
|
||||||
name = models.CharField(max_length=128)
|
name = models.CharField(max_length=128)
|
||||||
description = models.TextField(blank=True)
|
description = models.TextField(blank=True)
|
||||||
icon = models.CharField(max_length=16, blank=True, null=True)
|
icon = models.CharField(max_length=16, blank=True, null=True)
|
||||||
shared = models.ManyToManyField(
|
shared = models.ManyToManyField(User, blank=True, related_name='shared_with')
|
||||||
User, blank=True, related_name='shared_with'
|
|
||||||
)
|
|
||||||
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||||
|
objects = ScopedManager(space='space')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
class RecipeBookEntry(models.Model):
|
class RecipeBookEntry(models.Model, PermissionModelMixin):
|
||||||
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
|
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
|
||||||
book = models.ForeignKey(RecipeBook, on_delete=models.CASCADE)
|
book = models.ForeignKey(RecipeBook, on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
objects = ScopedManager(space='book__space')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_space_key():
|
||||||
|
return 'book', 'space'
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.recipe.name
|
return self.recipe.name
|
||||||
|
|
||||||
@ -355,29 +474,31 @@ class RecipeBookEntry(models.Model):
|
|||||||
unique_together = (('recipe', 'book'),)
|
unique_together = (('recipe', 'book'),)
|
||||||
|
|
||||||
|
|
||||||
class MealType(models.Model):
|
class MealType(models.Model, PermissionModelMixin):
|
||||||
name = models.CharField(max_length=128)
|
name = models.CharField(max_length=128)
|
||||||
order = models.IntegerField(default=0)
|
order = models.IntegerField(default=0)
|
||||||
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||||
|
objects = ScopedManager(space='space')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
class MealPlan(models.Model):
|
class MealPlan(models.Model, PermissionModelMixin):
|
||||||
recipe = models.ForeignKey(
|
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, blank=True, null=True)
|
||||||
Recipe, on_delete=models.CASCADE, blank=True, null=True
|
|
||||||
)
|
|
||||||
servings = models.DecimalField(default=1, max_digits=8, decimal_places=4)
|
servings = models.DecimalField(default=1, max_digits=8, decimal_places=4)
|
||||||
title = models.CharField(max_length=64, blank=True, default='')
|
title = models.CharField(max_length=64, blank=True, default='')
|
||||||
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
shared = models.ManyToManyField(
|
shared = models.ManyToManyField(User, blank=True, related_name='plan_share')
|
||||||
User, blank=True, related_name='plan_share'
|
|
||||||
)
|
|
||||||
meal_type = models.ForeignKey(MealType, on_delete=models.CASCADE)
|
meal_type = models.ForeignKey(MealType, on_delete=models.CASCADE)
|
||||||
note = models.TextField(blank=True)
|
note = models.TextField(blank=True)
|
||||||
date = models.DateField()
|
date = models.DateField()
|
||||||
|
|
||||||
|
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||||
|
objects = ScopedManager(space='space')
|
||||||
|
|
||||||
def get_label(self):
|
def get_label(self):
|
||||||
if self.title:
|
if self.title:
|
||||||
return self.title
|
return self.title
|
||||||
@ -390,12 +511,19 @@ class MealPlan(models.Model):
|
|||||||
return f'{self.get_label()} - {self.date} - {self.meal_type.name}'
|
return f'{self.get_label()} - {self.date} - {self.meal_type.name}'
|
||||||
|
|
||||||
|
|
||||||
class ShoppingListRecipe(models.Model):
|
class ShoppingListRecipe(models.Model, PermissionModelMixin):
|
||||||
recipe = models.ForeignKey(
|
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, null=True, blank=True)
|
||||||
Recipe, on_delete=models.CASCADE, null=True, blank=True
|
|
||||||
)
|
|
||||||
servings = models.DecimalField(default=1, max_digits=8, decimal_places=4)
|
servings = models.DecimalField(default=1, max_digits=8, decimal_places=4)
|
||||||
|
|
||||||
|
objects = ScopedManager(space='recipe__space')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_space_key():
|
||||||
|
return 'recipe', 'space'
|
||||||
|
|
||||||
|
def get_space(self):
|
||||||
|
return self.recipe.space
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'Shopping list recipe {self.id} - {self.recipe}'
|
return f'Shopping list recipe {self.id} - {self.recipe}'
|
||||||
|
|
||||||
@ -406,7 +534,7 @@ class ShoppingListRecipe(models.Model):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class ShoppingListEntry(models.Model):
|
class ShoppingListEntry(models.Model, PermissionModelMixin):
|
||||||
list_recipe = models.ForeignKey(ShoppingListRecipe, on_delete=models.CASCADE, null=True, blank=True)
|
list_recipe = models.ForeignKey(ShoppingListRecipe, on_delete=models.CASCADE, null=True, blank=True)
|
||||||
food = models.ForeignKey(Food, on_delete=models.CASCADE)
|
food = models.ForeignKey(Food, on_delete=models.CASCADE)
|
||||||
unit = models.ForeignKey(Unit, on_delete=models.CASCADE, null=True, blank=True)
|
unit = models.ForeignKey(Unit, on_delete=models.CASCADE, null=True, blank=True)
|
||||||
@ -414,9 +542,21 @@ class ShoppingListEntry(models.Model):
|
|||||||
order = models.IntegerField(default=0)
|
order = models.IntegerField(default=0)
|
||||||
checked = models.BooleanField(default=False)
|
checked = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
objects = ScopedManager(space='shoppinglist__space')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_space_key():
|
||||||
|
return 'shoppinglist', 'space'
|
||||||
|
|
||||||
|
def get_space(self):
|
||||||
|
return self.shoppinglist_set.first().space
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'Shopping list entry {self.id}'
|
return f'Shopping list entry {self.id}'
|
||||||
|
|
||||||
|
def get_shared(self):
|
||||||
|
return self.shoppinglist_set.first().shared.all()
|
||||||
|
|
||||||
def get_owner(self):
|
def get_owner(self):
|
||||||
try:
|
try:
|
||||||
return self.shoppinglist_set.first().created_by
|
return self.shoppinglist_set.first().created_by
|
||||||
@ -424,7 +564,7 @@ class ShoppingListEntry(models.Model):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class ShoppingList(models.Model):
|
class ShoppingList(models.Model, PermissionModelMixin):
|
||||||
uuid = models.UUIDField(default=uuid.uuid4)
|
uuid = models.UUIDField(default=uuid.uuid4)
|
||||||
note = models.TextField(blank=True, null=True)
|
note = models.TextField(blank=True, null=True)
|
||||||
recipes = models.ManyToManyField(ShoppingListRecipe, blank=True)
|
recipes = models.ManyToManyField(ShoppingListRecipe, blank=True)
|
||||||
@ -435,16 +575,22 @@ class ShoppingList(models.Model):
|
|||||||
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||||
|
objects = ScopedManager(space='space')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'Shopping list {self.id}'
|
return f'Shopping list {self.id}'
|
||||||
|
|
||||||
|
|
||||||
class ShareLink(models.Model):
|
class ShareLink(models.Model, PermissionModelMixin):
|
||||||
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
|
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
|
||||||
uuid = models.UUIDField(default=uuid.uuid4)
|
uuid = models.UUIDField(default=uuid.uuid4)
|
||||||
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||||
|
objects = ScopedManager(space='space')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{self.recipe} - {self.uuid}'
|
return f'{self.recipe} - {self.uuid}'
|
||||||
|
|
||||||
@ -453,7 +599,7 @@ def default_valid_until():
|
|||||||
return date.today() + timedelta(days=14)
|
return date.today() + timedelta(days=14)
|
||||||
|
|
||||||
|
|
||||||
class InviteLink(models.Model):
|
class InviteLink(models.Model, PermissionModelMixin):
|
||||||
uuid = models.UUIDField(default=uuid.uuid4)
|
uuid = models.UUIDField(default=uuid.uuid4)
|
||||||
username = models.CharField(blank=True, max_length=64)
|
username = models.CharField(blank=True, max_length=64)
|
||||||
group = models.ForeignKey(Group, on_delete=models.CASCADE)
|
group = models.ForeignKey(Group, on_delete=models.CASCADE)
|
||||||
@ -464,25 +610,49 @@ class InviteLink(models.Model):
|
|||||||
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||||
|
objects = ScopedManager(space='space')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{self.uuid}'
|
return f'{self.uuid}'
|
||||||
|
|
||||||
|
|
||||||
class CookLog(models.Model):
|
class CookLog(models.Model, PermissionModelMixin):
|
||||||
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
|
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
|
||||||
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
created_at = models.DateTimeField(default=timezone.now)
|
created_at = models.DateTimeField(default=timezone.now)
|
||||||
rating = models.IntegerField(null=True)
|
rating = models.IntegerField(null=True)
|
||||||
servings = models.IntegerField(default=0)
|
servings = models.IntegerField(default=0)
|
||||||
|
|
||||||
|
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||||
|
objects = ScopedManager(space='space')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.recipe.name
|
return self.recipe.name
|
||||||
|
|
||||||
|
|
||||||
class ViewLog(models.Model):
|
class ViewLog(models.Model, PermissionModelMixin):
|
||||||
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
|
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
|
||||||
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||||
|
objects = ScopedManager(space='space')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.recipe.name
|
return self.recipe.name
|
||||||
|
|
||||||
|
|
||||||
|
class ImportLog(models.Model, PermissionModelMixin):
|
||||||
|
type = models.CharField(max_length=32)
|
||||||
|
running = models.BooleanField(default=True)
|
||||||
|
msg = models.TextField(default="")
|
||||||
|
keyword = models.ForeignKey(Keyword, null=True, blank=True, on_delete=models.SET_NULL)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
objects = ScopedManager(space='space')
|
||||||
|
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.created_at}:{self.type}"
|
||||||
|
@ -35,14 +35,14 @@ class Dropbox(Provider):
|
|||||||
# TODO check if has_more is set and import that as well
|
# TODO check if has_more is set and import that as well
|
||||||
for recipe in recipes['entries']:
|
for recipe in recipes['entries']:
|
||||||
path = recipe['path_lower']
|
path = recipe['path_lower']
|
||||||
if not Recipe.objects.filter(file_path__iexact=path).exists() \
|
if not Recipe.objects.filter(file_path__iexact=path, space=monitor.space).exists() and not RecipeImport.objects.filter(file_path=path, space=monitor.space).exists():
|
||||||
and not RecipeImport.objects.filter(file_path=path).exists(): # noqa: E501
|
|
||||||
name = os.path.splitext(recipe['name'])[0]
|
name = os.path.splitext(recipe['name'])[0]
|
||||||
new_recipe = RecipeImport(
|
new_recipe = RecipeImport(
|
||||||
name=name,
|
name=name,
|
||||||
file_path=path,
|
file_path=path,
|
||||||
storage=monitor.storage,
|
storage=monitor.storage,
|
||||||
file_uid=recipe['id']
|
file_uid=recipe['id'],
|
||||||
|
space=monitor.space,
|
||||||
)
|
)
|
||||||
new_recipe.save()
|
new_recipe.save()
|
||||||
import_count += 1
|
import_count += 1
|
||||||
@ -50,7 +50,7 @@ class Dropbox(Provider):
|
|||||||
log_entry = SyncLog(
|
log_entry = SyncLog(
|
||||||
status='SUCCESS',
|
status='SUCCESS',
|
||||||
msg='Imported ' + str(import_count) + ' recipes',
|
msg='Imported ' + str(import_count) + ' recipes',
|
||||||
sync=monitor
|
sync=monitor,
|
||||||
)
|
)
|
||||||
log_entry.save()
|
log_entry.save()
|
||||||
|
|
||||||
@ -104,9 +104,7 @@ class Dropbox(Provider):
|
|||||||
recipe.link = Dropbox.get_share_link(recipe)
|
recipe.link = Dropbox.get_share_link(recipe)
|
||||||
recipe.save()
|
recipe.save()
|
||||||
|
|
||||||
response = requests.get(
|
response = requests.get(recipe.link.replace('www.dropbox.', 'dl.dropboxusercontent.'))
|
||||||
recipe.link.replace('www.dropbox.', 'dl.dropboxusercontent.')
|
|
||||||
)
|
|
||||||
|
|
||||||
return io.BytesIO(response.content)
|
return io.BytesIO(response.content)
|
||||||
|
|
||||||
|
@ -18,13 +18,13 @@ class Local(Provider):
|
|||||||
import_count = 0
|
import_count = 0
|
||||||
for file in files:
|
for file in files:
|
||||||
path = monitor.path + '/' + file
|
path = monitor.path + '/' + file
|
||||||
if not Recipe.objects.filter(file_path__iexact=path).exists() \
|
if not Recipe.objects.filter(file_path__iexact=path, space=monitor.space).exists() and not RecipeImport.objects.filter(file_path=path, space=monitor.space).exists():
|
||||||
and not RecipeImport.objects.filter(file_path=path).exists(): # noqa: E501
|
|
||||||
name = os.path.splitext(file)[0]
|
name = os.path.splitext(file)[0]
|
||||||
new_recipe = RecipeImport(
|
new_recipe = RecipeImport(
|
||||||
name=name,
|
name=name,
|
||||||
file_path=path,
|
file_path=path,
|
||||||
storage=monitor.storage
|
storage=monitor.storage,
|
||||||
|
space=monitor.space,
|
||||||
)
|
)
|
||||||
new_recipe.save()
|
new_recipe.save()
|
||||||
import_count += 1
|
import_count += 1
|
||||||
@ -32,7 +32,7 @@ class Local(Provider):
|
|||||||
log_entry = SyncLog(
|
log_entry = SyncLog(
|
||||||
status='SUCCESS',
|
status='SUCCESS',
|
||||||
msg='Imported ' + str(import_count) + ' recipes',
|
msg='Imported ' + str(import_count) + ' recipes',
|
||||||
sync=monitor
|
sync=monitor,
|
||||||
)
|
)
|
||||||
log_entry.save()
|
log_entry.save()
|
||||||
|
|
||||||
|
@ -34,13 +34,13 @@ class Nextcloud(Provider):
|
|||||||
import_count = 0
|
import_count = 0
|
||||||
for file in files:
|
for file in files:
|
||||||
path = monitor.path + '/' + file
|
path = monitor.path + '/' + file
|
||||||
if not Recipe.objects.filter(file_path__iexact=path).exists() \
|
if not Recipe.objects.filter(file_path__iexact=path, space=monitor.space).exists() and not RecipeImport.objects.filter(file_path=path, space=monitor.space).exists():
|
||||||
and not RecipeImport.objects.filter(file_path=path).exists(): # noqa: E501
|
|
||||||
name = os.path.splitext(file)[0]
|
name = os.path.splitext(file)[0]
|
||||||
new_recipe = RecipeImport(
|
new_recipe = RecipeImport(
|
||||||
name=name,
|
name=name,
|
||||||
file_path=path,
|
file_path=path,
|
||||||
storage=monitor.storage
|
storage=monitor.storage,
|
||||||
|
space=monitor.space,
|
||||||
)
|
)
|
||||||
new_recipe.save()
|
new_recipe.save()
|
||||||
import_count += 1
|
import_count += 1
|
||||||
@ -48,7 +48,7 @@ class Nextcloud(Provider):
|
|||||||
log_entry = SyncLog(
|
log_entry = SyncLog(
|
||||||
status='SUCCESS',
|
status='SUCCESS',
|
||||||
msg='Imported ' + str(import_count) + ' recipes',
|
msg='Imported ' + str(import_count) + ' recipes',
|
||||||
sync=monitor
|
sync=monitor,
|
||||||
)
|
)
|
||||||
log_entry.save()
|
log_entry.save()
|
||||||
|
|
||||||
@ -68,14 +68,7 @@ class Nextcloud(Provider):
|
|||||||
|
|
||||||
data = {'path': recipe.file_path, 'shareType': 3}
|
data = {'path': recipe.file_path, 'shareType': 3}
|
||||||
|
|
||||||
r = requests.post(
|
r = requests.post(url, headers=headers, auth=HTTPBasicAuth(recipe.storage.username, recipe.storage.password), data=data)
|
||||||
url,
|
|
||||||
headers=headers,
|
|
||||||
auth=HTTPBasicAuth(
|
|
||||||
recipe.storage.username, recipe.storage.password
|
|
||||||
),
|
|
||||||
data=data
|
|
||||||
)
|
|
||||||
|
|
||||||
response_json = r.json()
|
response_json = r.json()
|
||||||
|
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.db.models import QuerySet
|
||||||
from drf_writable_nested import (UniqueFieldsMixin,
|
from drf_writable_nested import (UniqueFieldsMixin,
|
||||||
WritableNestedModelSerializer)
|
WritableNestedModelSerializer)
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError, NotAuthenticated, NotFound, ParseError
|
||||||
|
from rest_framework.fields import ModelField
|
||||||
|
from rest_framework.serializers import BaseSerializer, Serializer
|
||||||
|
|
||||||
from cookbook.models import (Comment, CookLog, Food, Ingredient, Keyword,
|
from cookbook.models import (Comment, CookLog, Food, Ingredient, Keyword,
|
||||||
MealPlan, MealType, NutritionInformation, Recipe,
|
MealPlan, MealType, NutritionInformation, Recipe,
|
||||||
RecipeBook, RecipeBookEntry, RecipeImport,
|
RecipeBook, RecipeBookEntry, RecipeImport,
|
||||||
ShareLink, ShoppingList, ShoppingListEntry,
|
ShareLink, ShoppingList, ShoppingListEntry,
|
||||||
ShoppingListRecipe, Step, Storage, Sync, SyncLog,
|
ShoppingListRecipe, Step, Storage, Sync, SyncLog,
|
||||||
Unit, UserPreference, ViewLog, SupermarketCategory, Supermarket, SupermarketCategoryRelation)
|
Unit, UserPreference, ViewLog, SupermarketCategory, Supermarket, SupermarketCategoryRelation, ImportLog)
|
||||||
from cookbook.templatetags.custom_tags import markdown
|
from cookbook.templatetags.custom_tags import markdown
|
||||||
|
|
||||||
|
|
||||||
@ -39,6 +42,38 @@ class CustomDecimalField(serializers.Field):
|
|||||||
raise ValidationError('A valid number is required')
|
raise ValidationError('A valid number is required')
|
||||||
|
|
||||||
|
|
||||||
|
class SpaceFilterSerializer(serializers.ListSerializer):
|
||||||
|
|
||||||
|
def to_representation(self, data):
|
||||||
|
if type(data) == QuerySet and data.query.is_sliced:
|
||||||
|
# if query is sliced it came from api request not nested serializer
|
||||||
|
return super().to_representation(data)
|
||||||
|
if self.child.Meta.model == User:
|
||||||
|
data = data.filter(userpreference__space=self.context['request'].space)
|
||||||
|
else:
|
||||||
|
data = data.filter(**{'__'.join(data.model.get_space_key()): self.context['request'].space})
|
||||||
|
return super().to_representation(data)
|
||||||
|
|
||||||
|
|
||||||
|
class SpacedModelSerializer(serializers.ModelSerializer):
|
||||||
|
def create(self, validated_data):
|
||||||
|
validated_data['space'] = self.context['request'].space
|
||||||
|
return super().create(validated_data)
|
||||||
|
|
||||||
|
|
||||||
|
class MealTypeSerializer(SpacedModelSerializer):
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
validated_data['created_by'] = self.context['request'].user
|
||||||
|
return super().create(validated_data)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
list_serializer_class = SpaceFilterSerializer
|
||||||
|
model = MealType
|
||||||
|
fields = ('id', 'name', 'order', 'created_by')
|
||||||
|
read_only_fields = ('created_by',)
|
||||||
|
|
||||||
|
|
||||||
class UserNameSerializer(WritableNestedModelSerializer):
|
class UserNameSerializer(WritableNestedModelSerializer):
|
||||||
username = serializers.SerializerMethodField('get_user_label')
|
username = serializers.SerializerMethodField('get_user_label')
|
||||||
|
|
||||||
@ -46,11 +81,18 @@ class UserNameSerializer(WritableNestedModelSerializer):
|
|||||||
return obj.get_user_name()
|
return obj.get_user_name()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
list_serializer_class = SpaceFilterSerializer
|
||||||
model = User
|
model = User
|
||||||
fields = ('id', 'username')
|
fields = ('id', 'username')
|
||||||
|
|
||||||
|
|
||||||
class UserPreferenceSerializer(serializers.ModelSerializer):
|
class UserPreferenceSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
if validated_data['user'] != self.context['request'].user:
|
||||||
|
raise NotFound()
|
||||||
|
return super().create(validated_data)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = UserPreference
|
model = UserPreference
|
||||||
fields = (
|
fields = (
|
||||||
@ -58,10 +100,14 @@ class UserPreferenceSerializer(serializers.ModelSerializer):
|
|||||||
'search_style', 'show_recent', 'plan_share', 'ingredient_decimals',
|
'search_style', 'show_recent', 'plan_share', 'ingredient_decimals',
|
||||||
'comments'
|
'comments'
|
||||||
)
|
)
|
||||||
read_only_fields = ['user']
|
|
||||||
|
|
||||||
|
|
||||||
class StorageSerializer(serializers.ModelSerializer):
|
class StorageSerializer(SpacedModelSerializer):
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
validated_data['created_by'] = self.context['request'].user
|
||||||
|
return super().create(validated_data)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Storage
|
model = Storage
|
||||||
fields = (
|
fields = (
|
||||||
@ -69,13 +115,15 @@ class StorageSerializer(serializers.ModelSerializer):
|
|||||||
'token', 'created_by'
|
'token', 'created_by'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
read_only_fields = ('created_by',)
|
||||||
|
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'password': {'write_only': True},
|
'password': {'write_only': True},
|
||||||
'token': {'write_only': True},
|
'token': {'write_only': True},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class SyncSerializer(serializers.ModelSerializer):
|
class SyncSerializer(SpacedModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Sync
|
model = Sync
|
||||||
fields = (
|
fields = (
|
||||||
@ -84,7 +132,7 @@ class SyncSerializer(serializers.ModelSerializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class SyncLogSerializer(serializers.ModelSerializer):
|
class SyncLogSerializer(SpacedModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SyncLog
|
model = SyncLog
|
||||||
fields = ('id', 'sync', 'status', 'msg', 'created_at')
|
fields = ('id', 'sync', 'status', 'msg', 'created_at')
|
||||||
@ -97,6 +145,7 @@ class KeywordLabelSerializer(serializers.ModelSerializer):
|
|||||||
return str(obj)
|
return str(obj)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
list_serializer_class = SpaceFilterSerializer
|
||||||
model = Keyword
|
model = Keyword
|
||||||
fields = (
|
fields = (
|
||||||
'id', 'label',
|
'id', 'label',
|
||||||
@ -111,17 +160,13 @@ class KeywordSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
|
|||||||
return str(obj)
|
return str(obj)
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
# since multi select tags dont have id's
|
obj, created = Keyword.objects.get_or_create(name=validated_data['name'], space=self.context['request'].space)
|
||||||
# duplicate names might be routed to create
|
|
||||||
obj, created = Keyword.objects.get_or_create(name=validated_data['name'])
|
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
list_serializer_class = SpaceFilterSerializer
|
||||||
model = Keyword
|
model = Keyword
|
||||||
fields = (
|
fields = ('id', 'name', 'icon', 'label', 'description', 'created_at', 'updated_at')
|
||||||
'id', 'name', 'icon', 'label', 'description',
|
|
||||||
'created_at', 'updated_at'
|
|
||||||
)
|
|
||||||
|
|
||||||
read_only_fields = ('id',)
|
read_only_fields = ('id',)
|
||||||
|
|
||||||
@ -129,9 +174,7 @@ class KeywordSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
|
|||||||
class UnitSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
|
class UnitSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
# since multi select tags dont have id's
|
obj, created = Unit.objects.get_or_create(name=validated_data['name'], space=self.context['request'].space)
|
||||||
# duplicate names might be routed to create
|
|
||||||
obj, created = Unit.objects.get_or_create(name=validated_data['name'])
|
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -143,9 +186,7 @@ class UnitSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
|
|||||||
class SupermarketCategorySerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
|
class SupermarketCategorySerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
# since multi select tags dont have id's
|
obj, created = SupermarketCategory.objects.get_or_create(name=validated_data['name'], space=self.context['request'].space)
|
||||||
# duplicate names might be routed to create
|
|
||||||
obj, created = SupermarketCategory.objects.get_or_create(name=validated_data['name'])
|
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
@ -156,7 +197,7 @@ class SupermarketCategorySerializer(UniqueFieldsMixin, WritableNestedModelSerial
|
|||||||
fields = ('id', 'name')
|
fields = ('id', 'name')
|
||||||
|
|
||||||
|
|
||||||
class SupermarketCategoryRelationSerializer(serializers.ModelSerializer):
|
class SupermarketCategoryRelationSerializer(SpacedModelSerializer):
|
||||||
category = SupermarketCategorySerializer()
|
category = SupermarketCategorySerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -164,7 +205,7 @@ class SupermarketCategoryRelationSerializer(serializers.ModelSerializer):
|
|||||||
fields = ('id', 'category', 'supermarket', 'order')
|
fields = ('id', 'category', 'supermarket', 'order')
|
||||||
|
|
||||||
|
|
||||||
class SupermarketSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
|
class SupermarketSerializer(UniqueFieldsMixin, SpacedModelSerializer):
|
||||||
category_to_supermarket = SupermarketCategoryRelationSerializer(many=True, read_only=True)
|
category_to_supermarket = SupermarketCategoryRelationSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -176,9 +217,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
|
|||||||
supermarket_category = SupermarketCategorySerializer(allow_null=True, required=False)
|
supermarket_category = SupermarketCategorySerializer(allow_null=True, required=False)
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
# since multi select tags dont have id's
|
obj, created = Food.objects.get_or_create(name=validated_data['name'], space=self.context['request'].space)
|
||||||
# duplicate names might be routed to create
|
|
||||||
obj, created = Food.objects.get_or_create(name=validated_data['name'])
|
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
@ -256,6 +295,7 @@ class RecipeSerializer(WritableNestedModelSerializer):
|
|||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
validated_data['created_by'] = self.context['request'].user
|
validated_data['created_by'] = self.context['request'].user
|
||||||
|
validated_data['space'] = self.context['request'].space
|
||||||
return super().create(validated_data)
|
return super().create(validated_data)
|
||||||
|
|
||||||
|
|
||||||
@ -265,7 +305,7 @@ class RecipeImageSerializer(WritableNestedModelSerializer):
|
|||||||
fields = ['image', ]
|
fields = ['image', ]
|
||||||
|
|
||||||
|
|
||||||
class RecipeImportSerializer(serializers.ModelSerializer):
|
class RecipeImportSerializer(SpacedModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RecipeImport
|
model = RecipeImport
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
@ -277,26 +317,32 @@ class CommentSerializer(serializers.ModelSerializer):
|
|||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
class RecipeBookSerializer(serializers.ModelSerializer):
|
class RecipeBookSerializer(SpacedModelSerializer):
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
validated_data['created_by'] = self.context['request'].user
|
||||||
|
return super().create(validated_data)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RecipeBook
|
model = RecipeBook
|
||||||
fields = '__all__'
|
fields = ('id', 'name', 'description', 'icon', 'shared', 'created_by')
|
||||||
read_only_fields = ['id', 'created_by']
|
read_only_fields = ('created_by',)
|
||||||
|
|
||||||
|
|
||||||
class RecipeBookEntrySerializer(serializers.ModelSerializer):
|
class RecipeBookEntrySerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
book = validated_data['book']
|
||||||
|
if not book.get_owner() == self.context['request'].user:
|
||||||
|
raise NotFound(detail=None, code=None)
|
||||||
|
return super().create(validated_data)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RecipeBookEntry
|
model = RecipeBookEntry
|
||||||
fields = '__all__'
|
fields = ('id', 'book', 'recipe',)
|
||||||
|
|
||||||
|
|
||||||
class MealTypeSerializer(serializers.ModelSerializer):
|
class MealPlanSerializer(SpacedModelSerializer):
|
||||||
class Meta:
|
|
||||||
model = MealType
|
|
||||||
fields = '__all__'
|
|
||||||
|
|
||||||
|
|
||||||
class MealPlanSerializer(serializers.ModelSerializer):
|
|
||||||
recipe_name = serializers.ReadOnlyField(source='recipe.name')
|
recipe_name = serializers.ReadOnlyField(source='recipe.name')
|
||||||
meal_type_name = serializers.ReadOnlyField(source='meal_type.name')
|
meal_type_name = serializers.ReadOnlyField(source='meal_type.name')
|
||||||
note_markdown = serializers.SerializerMethodField('get_note_markdown')
|
note_markdown = serializers.SerializerMethodField('get_note_markdown')
|
||||||
@ -305,6 +351,10 @@ class MealPlanSerializer(serializers.ModelSerializer):
|
|||||||
def get_note_markdown(self, obj):
|
def get_note_markdown(self, obj):
|
||||||
return markdown(obj.note)
|
return markdown(obj.note)
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
validated_data['created_by'] = self.context['request'].user
|
||||||
|
return super().create(validated_data)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = MealPlan
|
model = MealPlan
|
||||||
fields = (
|
fields = (
|
||||||
@ -312,6 +362,7 @@ class MealPlanSerializer(serializers.ModelSerializer):
|
|||||||
'date', 'meal_type', 'created_by', 'shared', 'recipe_name',
|
'date', 'meal_type', 'created_by', 'shared', 'recipe_name',
|
||||||
'meal_type_name'
|
'meal_type_name'
|
||||||
)
|
)
|
||||||
|
read_only_fields = ('created_by',)
|
||||||
|
|
||||||
|
|
||||||
class ShoppingListRecipeSerializer(serializers.ModelSerializer):
|
class ShoppingListRecipeSerializer(serializers.ModelSerializer):
|
||||||
@ -348,13 +399,18 @@ class ShoppingListSerializer(WritableNestedModelSerializer):
|
|||||||
shared = UserNameSerializer(many=True)
|
shared = UserNameSerializer(many=True)
|
||||||
supermarket = SupermarketSerializer(allow_null=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:
|
class Meta:
|
||||||
model = ShoppingList
|
model = ShoppingList
|
||||||
fields = (
|
fields = (
|
||||||
'id', 'uuid', 'note', 'recipes', 'entries',
|
'id', 'uuid', 'note', 'recipes', 'entries',
|
||||||
'shared', 'finished', 'supermarket', 'created_by', 'created_at'
|
'shared', 'finished', 'supermarket', 'created_by', 'created_at'
|
||||||
)
|
)
|
||||||
read_only_fields = ('id',)
|
read_only_fields = ('id', 'created_by',)
|
||||||
|
|
||||||
|
|
||||||
class ShoppingListAutoSyncSerializer(WritableNestedModelSerializer):
|
class ShoppingListAutoSyncSerializer(WritableNestedModelSerializer):
|
||||||
@ -366,27 +422,48 @@ class ShoppingListAutoSyncSerializer(WritableNestedModelSerializer):
|
|||||||
read_only_fields = ('id',)
|
read_only_fields = ('id',)
|
||||||
|
|
||||||
|
|
||||||
class ShareLinkSerializer(serializers.ModelSerializer):
|
class ShareLinkSerializer(SpacedModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ShareLink
|
model = ShareLink
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
class CookLogSerializer(serializers.ModelSerializer):
|
class CookLogSerializer(serializers.ModelSerializer):
|
||||||
def create(self, validated_data): # TODO make mixin
|
def create(self, validated_data):
|
||||||
validated_data['created_by'] = self.context['request'].user
|
validated_data['created_by'] = self.context['request'].user
|
||||||
|
validated_data['space'] = self.context['request'].space
|
||||||
return super().create(validated_data)
|
return super().create(validated_data)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CookLog
|
model = CookLog
|
||||||
fields = '__all__'
|
fields = ('id', 'recipe', 'servings', 'rating', 'created_by', 'created_at')
|
||||||
read_only_fields = ('id', 'created_by')
|
read_only_fields = ('id', 'created_by')
|
||||||
|
|
||||||
|
|
||||||
class ViewLogSerializer(serializers.ModelSerializer):
|
class ViewLogSerializer(serializers.ModelSerializer):
|
||||||
|
def create(self, validated_data):
|
||||||
|
validated_data['created_by'] = self.context['request'].user
|
||||||
|
validated_data['space'] = self.context['request'].space
|
||||||
|
return super().create(validated_data)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ViewLog
|
model = ViewLog
|
||||||
fields = '__all__'
|
fields = ('id', 'recipe', 'created_by', 'created_at')
|
||||||
|
read_only_fields = ('created_by',)
|
||||||
|
|
||||||
|
|
||||||
|
class ImportLogSerializer(serializers.ModelSerializer):
|
||||||
|
keyword = KeywordSerializer(read_only=True)
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
validated_data['created_by'] = self.context['request'].user
|
||||||
|
validated_data['space'] = self.context['request'].space
|
||||||
|
return super().create(validated_data)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ImportLog
|
||||||
|
fields = ('id', 'type', 'msg', 'running', 'keyword', 'created_by', 'created_at')
|
||||||
|
read_only_fields = ('created_by',)
|
||||||
|
|
||||||
|
|
||||||
# Export/Import Serializers
|
# Export/Import Serializers
|
||||||
@ -455,4 +532,5 @@ class RecipeExportSerializer(WritableNestedModelSerializer):
|
|||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
validated_data['created_by'] = self.context['request'].user
|
validated_data['created_by'] = self.context['request'].user
|
||||||
|
validated_data['space'] = self.context['request'].space
|
||||||
return super().create(validated_data)
|
return super().create(validated_data)
|
||||||
|
File diff suppressed because one or more lines are too long
1
cookbook/static/vue/import_response_view.html
Normal file
1
cookbook/static/vue/import_response_view.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Vue App</title><link href="css/chunk-vendors.css" rel="preload" as="style"><link href="js/chunk-vendors.js" rel="preload" as="script"><link href="js/import_response_view.js" rel="preload" as="script"><link href="css/chunk-vendors.css" rel="stylesheet"><link rel="icon" type="image/png" sizes="32x32" href="img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="img/icons/favicon-16x16.png"><link rel="manifest" href="manifest.json"><meta name="theme-color" content="#4DBA87"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black"><meta name="apple-mobile-web-app-title" content="Recipes"><link rel="apple-touch-icon" href="img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="img/icons/safari-pinned-tab.svg" color="#4DBA87"><meta name="msapplication-TileImage" content="img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><div id="app"></div><script src="js/chunk-vendors.js"></script></body></html>
|
File diff suppressed because one or more lines are too long
1
cookbook/static/vue/js/import_response_view.js
Normal file
1
cookbook/static/vue/js/import_response_view.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
34
cookbook/templates/import_response.html
Normal file
34
cookbook/templates/import_response.html
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load render_bundle from webpack_loader %}
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load l10n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans 'Import' %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div id="app">
|
||||||
|
<import-response-view></import-response-view>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block script %}
|
||||||
|
<script src="{% url 'javascript-catalog' %}"></script>
|
||||||
|
|
||||||
|
{% if debug %}
|
||||||
|
<script src="{% url 'js_reverse' %}"></script>
|
||||||
|
{% else %}
|
||||||
|
<script src="{% static 'django_js_reverse/reverse.js' %}"></script>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<script type="application/javascript">
|
||||||
|
window.IMPORT_ID = {{pk}};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% render_bundle 'chunk-vendors' %}
|
||||||
|
{% render_bundle 'import_response_view' %}
|
||||||
|
{% endblock %}
|
@ -2,7 +2,7 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}{% trans "Offline" %}{% endblock %}
|
{% block title %}{% trans "No Permissions" %}{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@ -12,7 +12,12 @@
|
|||||||
<h1 class="">{% trans 'No Permissions' %}</h1>
|
<h1 class="">{% trans 'No Permissions' %}</h1>
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
<span>{% trans 'You do not have any groups and therefor cannot use this application. Please contact your administrator.' %}</span> <br/>
|
|
||||||
|
<span>
|
||||||
|
{% trans 'You do not have any groups and therefor cannot use this application.' %}
|
||||||
|
{% trans 'Please contact your administrator.' %}
|
||||||
|
</span>
|
||||||
|
<br/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
20
cookbook/templates/no_perm_info.html
Normal file
20
cookbook/templates/no_perm_info.html
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "No Permission" %}{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div style="text-align: center">
|
||||||
|
|
||||||
|
<h1 class="">{% trans 'No Permission' %}</h1>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<span>{% trans 'You do not have the required permissions to view this page or perform this action.' %} {% trans 'Please contact your administrator.' %}</span> <br/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
20
cookbook/templates/no_space_info.html
Normal file
20
cookbook/templates/no_space_info.html
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "No Space" %}{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div style="text-align: center">
|
||||||
|
|
||||||
|
<h1 class="">{% trans 'No Space' %}</h1>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<span>{% trans 'You are not a member of any space.' %} {% trans 'Please contact your administrator.' %}</span> <br/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -122,8 +122,9 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody is="draggable" :list="c.entries" tag="tbody" group="people" @sort="sortEntries"
|
<tbody is="draggable" :list="c.entries" tag="tbody" group="people" @sort="sortEntries"
|
||||||
@change="dragChanged(c, $event)">
|
@change="dragChanged(c, $event)" handle=".handle">
|
||||||
<tr v-for="(element, index) in c.entries" :key="element.id">
|
<tr v-for="(element, index) in c.entries" :key="element.id"
|
||||||
|
v-bind:class="{ 'text-muted': element.checked }">
|
||||||
<td class="handle"><i class="fas fa-sort"></i></td>
|
<td class="handle"><i class="fas fa-sort"></i></td>
|
||||||
<td>[[element.amount]]</td>
|
<td>[[element.amount]]</td>
|
||||||
<td>[[element.unit.name]]</td>
|
<td>[[element.unit.name]]</td>
|
||||||
@ -154,7 +155,8 @@
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input id="id_simple_entry" class="form-control" v-model="simple_entry">
|
<input id="id_simple_entry" class="form-control" v-model="simple_entry">
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<button class="btn btn-outline-secondary" type="button"><i class="fa fa-plus"></i>
|
<button class="btn btn-outline-secondary" type="button" @click="addSimpleEntry()"><i
|
||||||
|
class="fa fa-plus"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -349,6 +351,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
<b-modal id="id_modal_export" title="{% trans 'Copy/Export' %}">
|
<b-modal id="id_modal_export" title="{% trans 'Copy/Export' %}">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -418,6 +422,8 @@
|
|||||||
users_loading: false,
|
users_loading: false,
|
||||||
onLine: navigator.onLine,
|
onLine: navigator.onLine,
|
||||||
simple_entry: '',
|
simple_entry: '',
|
||||||
|
auto_sync_blocked: false,
|
||||||
|
auto_sync_running: false,
|
||||||
entry_mode_simple: $cookies.isKey('shopping_entry_mode_simple') ? ($cookies.get('shopping_entry_mode_simple') === 'true') : true,
|
entry_mode_simple: $cookies.isKey('shopping_entry_mode_simple') ? ($cookies.get('shopping_entry_mode_simple') === 'true') : true,
|
||||||
},
|
},
|
||||||
directives: {
|
directives: {
|
||||||
@ -448,7 +454,7 @@
|
|||||||
name: gettext('Uncategorized'),
|
name: gettext('Uncategorized'),
|
||||||
id: -1,
|
id: -1,
|
||||||
entries: [],
|
entries: [],
|
||||||
order: 99999999
|
order: -1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -542,6 +548,7 @@
|
|||||||
this.loadShoppingList()
|
this.loadShoppingList()
|
||||||
|
|
||||||
{% if recipes %}
|
{% if recipes %}
|
||||||
|
|
||||||
this.loading = true
|
this.loading = true
|
||||||
this.edit_mode = true
|
this.edit_mode = true
|
||||||
let loadingRecipes = []
|
let loadingRecipes = []
|
||||||
@ -556,7 +563,8 @@
|
|||||||
|
|
||||||
{% if request.user.userpreference.shopping_auto_sync > 0 %}
|
{% if request.user.userpreference.shopping_auto_sync > 0 %}
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
if ((this.shopping_list_id !== null) && !this.edit_mode && window.navigator.onLine) {
|
if ((this.shopping_list_id !== null) && !this.edit_mode && window.navigator.onLine && !this.auto_sync_blocked && !this.auto_sync_running) {
|
||||||
|
this.auto_sync_running = true
|
||||||
this.loadShoppingList(true)
|
this.loadShoppingList(true)
|
||||||
}
|
}
|
||||||
}, {% widthratio request.user.userpreference.shopping_auto_sync 1 1000 %})
|
}, {% widthratio request.user.userpreference.shopping_auto_sync 1 1000 %})
|
||||||
@ -605,6 +613,7 @@
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
loadInitialRecipe: function (recipe, servings) {
|
loadInitialRecipe: function (recipe, servings) {
|
||||||
|
servings = 1 //TODO temporary until i can actually fix the servings for this #453
|
||||||
return this.$http.get('{% url 'api:recipe-detail' 123456 %}'.replace('123456', recipe)).then((response) => {
|
return this.$http.get('{% url 'api:recipe-detail' 123456 %}'.replace('123456', recipe)).then((response) => {
|
||||||
this.addRecipeToList(response.data, servings)
|
this.addRecipeToList(response.data, servings)
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
@ -620,6 +629,7 @@
|
|||||||
this.shopping_list = response.body
|
this.shopping_list = response.body
|
||||||
this.loading = false
|
this.loading = false
|
||||||
} else {
|
} else {
|
||||||
|
if (!this.auto_sync_blocked) {
|
||||||
let check_map = {}
|
let check_map = {}
|
||||||
for (let e of response.body.entries) {
|
for (let e of response.body.entries) {
|
||||||
check_map[e.id] = {checked: e.checked}
|
check_map[e.id] = {checked: e.checked}
|
||||||
@ -631,6 +641,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.auto_sync_running = false
|
||||||
|
}
|
||||||
if (this.shopping_list.entries.length === 0) {
|
if (this.shopping_list.entries.length === 0) {
|
||||||
this.edit_mode = true
|
this.edit_mode = true
|
||||||
}
|
}
|
||||||
@ -743,18 +755,24 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
entryChecked: function (entry) {
|
entryChecked: function (entry) {
|
||||||
|
this.auto_sync_blocked = true
|
||||||
|
let updates = []
|
||||||
this.shopping_list.entries.forEach((item) => {
|
this.shopping_list.entries.forEach((item) => {
|
||||||
if (entry.entries.includes(item.id)) {
|
if (entry.entries.includes(item.id)) {
|
||||||
item.checked = entry.checked
|
item.checked = entry.checked
|
||||||
this.$http.put("{% url 'api:shoppinglistentry-detail' 123456 %}".replace('123456', item.id), item, {}).then((response) => {
|
updates.push(this.$http.put("{% url 'api:shoppinglistentry-detail' 123456 %}".replace('123456', item.id), item, {}).then((response) => {
|
||||||
|
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
this.makeToast(gettext('Error'), gettext('There was an error updating a resource!') + err.bodyText, 'danger')
|
this.makeToast(gettext('Error'), gettext('There was an error updating a resource!') + err.bodyText, 'danger')
|
||||||
this.loading = false
|
this.loading = false
|
||||||
})
|
}))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Promise.allSettled(updates).then(() => {
|
||||||
|
this.auto_sync_blocked = false
|
||||||
|
})
|
||||||
},
|
},
|
||||||
addEntry: function () {
|
addEntry: function () {
|
||||||
if (this.new_entry.food !== undefined) {
|
if (this.new_entry.food !== undefined) {
|
||||||
|
File diff suppressed because one or more lines are too long
@ -22,23 +22,6 @@
|
|||||||
<a href="{% url 'list_invite_link' %}" class="btn btn-success">{% trans 'Show Links' %}</a>
|
<a href="{% url 'list_invite_link' %}" class="btn btn-success">{% trans 'Show Links' %}</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<!--
|
|
||||||
<div class="col-md-6">
|
|
||||||
<h3>{% trans 'Backup & Restore' %}</h3>
|
|
||||||
<a href="{% url 'api_backup' %}" class="btn btn-success">{% trans 'Download Backup' %}</a>
|
|
||||||
|
|
||||||
<br/> <br/>
|
|
||||||
<div class="alert alert-danger" role="alert">
|
|
||||||
⚠️ Backups simply create a so called fixture. Fixtures are json files containing all your data (WITHOUT
|
|
||||||
MEDIA FILES) <br>
|
|
||||||
They can be imported into django by running <code style="color: white">manage.py loaddata [fixture-name]</code> <br>
|
|
||||||
It is planned to provide a better way of backing up and restoring data but it is not yet implemented.<br><br>
|
|
||||||
⚠️<b>Please make sure to setup a solid backup strategy on your server to save the Database and the <code style="color: white">mediafiles</code>
|
|
||||||
directory</b>⚠️
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
-->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
|
@ -30,6 +30,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input class="form-control" v-model="json_data" placeholder="{% trans 'Enter json directly' %}">
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button @click="loadRecipeJson()" class="btn btn-primary shadow-none" type="button"
|
||||||
|
id="id_btn_search"><i class="fas fa-search"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
<div v-if="loading" class="text-center">
|
<div v-if="loading" class="text-center">
|
||||||
@ -322,6 +335,21 @@
|
|||||||
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
|
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
loadRecipeJson: function () {
|
||||||
|
this.recipe_data = undefined
|
||||||
|
this.error = undefined
|
||||||
|
this.loading = true
|
||||||
|
this.$http.post("{% url 'api_recipe_from_json' %}", {'json': this.json_data}, {emulateJSON: true}).then((response) => {
|
||||||
|
console.log(response.data)
|
||||||
|
this.recipe_data = response.data;
|
||||||
|
this.loading = false
|
||||||
|
}).catch((err) => {
|
||||||
|
this.error = err.data
|
||||||
|
this.loading = false
|
||||||
|
console.log(err)
|
||||||
|
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
|
||||||
|
})
|
||||||
|
},
|
||||||
importRecipe: function () {
|
importRecipe: function () {
|
||||||
if (this.importing_recipe) {
|
if (this.importing_recipe) {
|
||||||
this.makeToast(gettext('Error'), gettext('Already importing the selected recipe, please wait!'), 'danger')
|
this.makeToast(gettext('Error'), gettext('Already importing the selected recipe, please wait!'), 'danger')
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import bleach
|
import bleach
|
||||||
import markdown as md
|
import markdown as md
|
||||||
from bleach_whitelist import markdown_attrs, markdown_tags
|
from bleach_allowlist import markdown_attrs, markdown_tags
|
||||||
from cookbook.helper.mdx_attributes import MarkdownFormatExtension
|
from cookbook.helper.mdx_attributes import MarkdownFormatExtension
|
||||||
from cookbook.helper.mdx_urlize import UrlizeExtension
|
from cookbook.helper.mdx_urlize import UrlizeExtension
|
||||||
from cookbook.models import Space, get_model_name
|
from cookbook.models import Space, get_model_name
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
from django.test import utils
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
# disables scoping error in all queries used inside the test FUNCTIONS
|
||||||
|
# FIXTURES need to have their own scopes_disabled!!
|
||||||
|
# This is done by hook pytest_fixture_setup in conftest.py for all non yield fixtures
|
||||||
|
utils.setup_databases = scopes_disabled()(utils.setup_databases)
|
116
cookbook/tests/api/test_api_cook_log.py
Normal file
116
cookbook/tests/api/test_api_cook_log.py
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.contrib import auth
|
||||||
|
from django.urls import reverse
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
from cookbook.models import Keyword, CookLog
|
||||||
|
|
||||||
|
LIST_URL = 'api:cooklog-list'
|
||||||
|
DETAIL_URL = 'api:cooklog-detail'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def obj_1(space_1, u1_s1, recipe_1_s1):
|
||||||
|
return CookLog.objects.create(recipe=recipe_1_s1, created_by=auth.get_user(u1_s1), space=space_1)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def obj_2(space_1, u1_s1, recipe_1_s1):
|
||||||
|
return CookLog.objects.create(recipe=recipe_1_s1, 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
|
||||||
|
|
||||||
|
|
||||||
|
@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'
|
||||||
|
)
|
||||||
|
response = json.loads(r.content)
|
||||||
|
assert r.status_code == arg[1]
|
||||||
|
if r.status_code == 200:
|
||||||
|
assert response['servings'] == 2
|
||||||
|
|
||||||
|
|
||||||
|
# TODO disabled until https://github.com/vabene1111/recipes/issues/484
|
||||||
|
|
||||||
|
# @pytest.mark.parametrize("arg", [
|
||||||
|
# ['a_u', 403],
|
||||||
|
# ['g1_s1', 201],
|
||||||
|
# ['u1_s1', 201],
|
||||||
|
# ['a1_s1', 201],
|
||||||
|
# ])
|
||||||
|
# def test_add(arg, request, u1_s2, u2_s1, recipe_1_s1):
|
||||||
|
# c = request.getfixturevalue(arg[0])
|
||||||
|
# r = c.post(
|
||||||
|
# reverse(LIST_URL),
|
||||||
|
# {'recipe': recipe_1_s1.id},
|
||||||
|
# content_type='application/json'
|
||||||
|
# )
|
||||||
|
# response = json.loads(r.content)
|
||||||
|
# assert r.status_code == arg[1]
|
||||||
|
# if r.status_code == 201:
|
||||||
|
# assert response['recipe'] == recipe_1_s1.id
|
||||||
|
# r = c.get(reverse(DETAIL_URL, args={response['id']}))
|
||||||
|
# assert r.status_code == 200
|
||||||
|
# r = u2_s1.get(reverse(DETAIL_URL, args={response['id']}))
|
||||||
|
# assert r.status_code == 404
|
||||||
|
# r = u1_s2.get(reverse(DETAIL_URL, args={response['id']}))
|
||||||
|
# assert r.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
with scopes_disabled():
|
||||||
|
assert CookLog.objects.count() == 0
|
@ -1,69 +1,148 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from cookbook.models import Food
|
import pytest
|
||||||
from cookbook.tests.views.test_views import TestViews
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
from cookbook.models import Food
|
||||||
|
|
||||||
|
LIST_URL = 'api:food-list'
|
||||||
|
DETAIL_URL = 'api:food-detail'
|
||||||
|
|
||||||
|
|
||||||
class TestApiUnit(TestViews):
|
@pytest.fixture()
|
||||||
|
def obj_1(space_1):
|
||||||
|
return Food.objects.get_or_create(name='test_1', space=space_1)[0]
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(TestApiUnit, self).setUp()
|
|
||||||
self.food_1 = Food.objects.create(
|
|
||||||
name='Beef'
|
|
||||||
)
|
|
||||||
self.food_2 = Food.objects.create(
|
|
||||||
name='Chicken'
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_keyword_list(self):
|
@pytest.fixture
|
||||||
# verify view permissions are applied accordingly
|
def obj_2(space_1):
|
||||||
self.batch_requests(
|
return Food.objects.get_or_create(name='test_2', space=space_1)[0]
|
||||||
[
|
|
||||||
(self.anonymous_client, 403),
|
|
||||||
(self.guest_client_1, 403),
|
|
||||||
(self.user_client_1, 200),
|
|
||||||
(self.admin_client_1, 200),
|
|
||||||
(self.superuser_client, 200)
|
|
||||||
],
|
|
||||||
reverse('api:food-list')
|
|
||||||
)
|
|
||||||
|
|
||||||
# verify storage is returned
|
|
||||||
r = self.user_client_1.get(reverse('api:food-list'))
|
@pytest.mark.parametrize("arg", [
|
||||||
self.assertEqual(r.status_code, 200)
|
['a_u', 403],
|
||||||
|
['g1_s1', 403],
|
||||||
|
['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)) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_filter(obj_1, obj_2, u1_s1):
|
||||||
|
r = u1_s1.get(reverse(LIST_URL))
|
||||||
|
assert r.status_code == 200
|
||||||
response = json.loads(r.content)
|
response = json.loads(r.content)
|
||||||
self.assertEqual(len(response), 2)
|
assert len(response) == 2
|
||||||
self.assertEqual(response[0]['name'], self.food_1.name)
|
assert response[0]['name'] == obj_1.name
|
||||||
|
|
||||||
r = self.user_client_1.get(f'{reverse("api:food-list")}?limit=1')
|
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?limit=1').content)
|
||||||
response = json.loads(r.content)
|
assert len(response) == 1
|
||||||
self.assertEqual(len(response), 1)
|
|
||||||
|
|
||||||
r = self.user_client_1.get(f'{reverse("api:food-list")}?query=Pork')
|
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query=chicken').content)
|
||||||
response = json.loads(r.content)
|
assert len(response) == 0
|
||||||
self.assertEqual(len(response), 0)
|
|
||||||
|
|
||||||
r = self.user_client_1.get(f'{reverse("api:food-list")}?query=Beef')
|
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query={obj_1.name[4:]}').content)
|
||||||
response = json.loads(r.content)
|
assert len(response) == 1
|
||||||
self.assertEqual(len(response), 1)
|
|
||||||
|
|
||||||
def test_keyword_update(self):
|
|
||||||
r = self.user_client_1.patch(
|
@pytest.mark.parametrize("arg", [
|
||||||
|
['a_u', 403],
|
||||||
|
['g1_s1', 403],
|
||||||
|
['u1_s1', 200],
|
||||||
|
['a1_s1', 200],
|
||||||
|
['g1_s2', 403],
|
||||||
|
['u1_s2', 404],
|
||||||
|
['a1_s2', 404],
|
||||||
|
])
|
||||||
|
def test_update(arg, request, obj_1):
|
||||||
|
c = request.getfixturevalue(arg[0])
|
||||||
|
r = c.patch(
|
||||||
reverse(
|
reverse(
|
||||||
'api:food-detail',
|
DETAIL_URL,
|
||||||
args={self.food_1.id}
|
args={obj_1.id}
|
||||||
),
|
),
|
||||||
{'name': 'new'},
|
{'name': 'new'},
|
||||||
content_type='application/json'
|
content_type='application/json'
|
||||||
)
|
)
|
||||||
response = json.loads(r.content)
|
response = json.loads(r.content)
|
||||||
self.assertEqual(r.status_code, 200)
|
assert r.status_code == arg[1]
|
||||||
self.assertEqual(response['name'], 'new')
|
if r.status_code == 200:
|
||||||
|
assert response['name'] == 'new'
|
||||||
|
|
||||||
def test_keyword_delete(self):
|
|
||||||
r = self.user_client_1.delete(
|
@pytest.mark.parametrize("arg", [
|
||||||
reverse('api:food-detail', args={self.food_1.id})
|
['a_u', 403],
|
||||||
|
['g1_s1', 403],
|
||||||
|
['u1_s1', 201],
|
||||||
|
['a1_s1', 201],
|
||||||
|
])
|
||||||
|
def test_add(arg, request, u1_s2):
|
||||||
|
c = request.getfixturevalue(arg[0])
|
||||||
|
r = c.post(
|
||||||
|
reverse(LIST_URL),
|
||||||
|
{'name': 'test'},
|
||||||
|
content_type='application/json'
|
||||||
)
|
)
|
||||||
self.assertEqual(r.status_code, 204)
|
response = json.loads(r.content)
|
||||||
self.assertEqual(Food.objects.count(), 1)
|
assert r.status_code == arg[1]
|
||||||
|
if r.status_code == 201:
|
||||||
|
assert response['name'] == 'test'
|
||||||
|
r = c.get(reverse(DETAIL_URL, args={response['id']}))
|
||||||
|
assert r.status_code == 200
|
||||||
|
r = u1_s2.get(reverse(DETAIL_URL, args={response['id']}))
|
||||||
|
assert r.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_duplicate(u1_s1, u1_s2, obj_1):
|
||||||
|
r = u1_s1.post(
|
||||||
|
reverse(LIST_URL),
|
||||||
|
{'name': obj_1.name},
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
response = json.loads(r.content)
|
||||||
|
assert r.status_code == 201
|
||||||
|
assert response['id'] == obj_1.id
|
||||||
|
|
||||||
|
r = u1_s2.post(
|
||||||
|
reverse(LIST_URL),
|
||||||
|
{'name': obj_1.name},
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
response = json.loads(r.content)
|
||||||
|
assert r.status_code == 201
|
||||||
|
assert response['id'] != obj_1.id
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
with scopes_disabled():
|
||||||
|
assert Food.objects.count() == 0
|
||||||
|
86
cookbook/tests/api/test_api_import_log.py
Normal file
86
cookbook/tests/api/test_api_import_log.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.contrib import auth
|
||||||
|
from django.urls import reverse
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
from cookbook.models import Keyword, CookLog, ViewLog, ImportLog
|
||||||
|
|
||||||
|
LIST_URL = 'api:importlog-list'
|
||||||
|
DETAIL_URL = 'api:importlog-detail'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def obj_1(space_1, u1_s1, recipe_1_s1):
|
||||||
|
return ImportLog.objects.create(type='test', created_by=auth.get_user(u1_s1), space=space_1)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def obj_2(space_1, u1_s1, recipe_1_s1):
|
||||||
|
return ImportLog.objects.create(type='test', created_by=auth.get_user(u1_s1), space=space_1)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("arg", [
|
||||||
|
['a_u', 403],
|
||||||
|
['g1_s1', 403],
|
||||||
|
['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)) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("arg", [
|
||||||
|
['a_u', 403],
|
||||||
|
['g1_s1', 403],
|
||||||
|
['u1_s1', 200],
|
||||||
|
['a1_s1', 200],
|
||||||
|
['g1_s2', 403],
|
||||||
|
['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}
|
||||||
|
),
|
||||||
|
{'msg': 'new'},
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
assert r.status_code == arg[1]
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
with scopes_disabled():
|
||||||
|
assert ImportLog.objects.count() == 0
|
106
cookbook/tests/api/test_api_ingredient.py
Normal file
106
cookbook/tests/api/test_api_ingredient.py
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.urls import reverse
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
from cookbook.models import Food, Ingredient
|
||||||
|
|
||||||
|
LIST_URL = 'api:ingredient-list'
|
||||||
|
DETAIL_URL = 'api:ingredient-detail'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("arg", [
|
||||||
|
['a_u', 403],
|
||||||
|
['g1_s1', 403],
|
||||||
|
['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(recipe_1_s1, u1_s1, u1_s2, space_2):
|
||||||
|
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 10
|
||||||
|
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0
|
||||||
|
|
||||||
|
with scopes_disabled():
|
||||||
|
recipe_1_s1.space = space_2
|
||||||
|
recipe_1_s1.save()
|
||||||
|
|
||||||
|
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 0
|
||||||
|
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 10
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("arg", [
|
||||||
|
['a_u', 403],
|
||||||
|
['g1_s1', 403],
|
||||||
|
['u1_s1', 200],
|
||||||
|
['a1_s1', 200],
|
||||||
|
['g1_s2', 403],
|
||||||
|
['u1_s2', 404],
|
||||||
|
['a1_s2', 404],
|
||||||
|
])
|
||||||
|
def test_update(arg, request, recipe_1_s1):
|
||||||
|
with scopes_disabled():
|
||||||
|
i = recipe_1_s1.steps.first().ingredients.first()
|
||||||
|
c = request.getfixturevalue(arg[0])
|
||||||
|
r = c.patch(
|
||||||
|
reverse(
|
||||||
|
DETAIL_URL,
|
||||||
|
args={i.id}
|
||||||
|
),
|
||||||
|
{'note': 'new'},
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
response = json.loads(r.content)
|
||||||
|
assert r.status_code == arg[1]
|
||||||
|
if r.status_code == 200:
|
||||||
|
assert response['note'] == 'new'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("arg", [
|
||||||
|
['a_u', 403],
|
||||||
|
['g1_s1', 403],
|
||||||
|
['u1_s1', 201],
|
||||||
|
['a1_s1', 201],
|
||||||
|
])
|
||||||
|
def test_add(arg, request, u1_s2):
|
||||||
|
c = request.getfixturevalue(arg[0])
|
||||||
|
r = c.post(
|
||||||
|
reverse(LIST_URL),
|
||||||
|
{'food': {'name': 'test'}, 'unit': {'name': 'test'}, 'amount': 1},
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
response = json.loads(r.content)
|
||||||
|
print(r)
|
||||||
|
assert r.status_code == arg[1]
|
||||||
|
if r.status_code == 201:
|
||||||
|
assert response['id'] == 1
|
||||||
|
r = c.get(reverse(DETAIL_URL, args={response['id']}))
|
||||||
|
assert r.status_code == 404 # ingredient is not linked to a recipe and therefore cannot be accessed
|
||||||
|
r = u1_s2.get(reverse(DETAIL_URL, args={response['id']}))
|
||||||
|
assert r.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete(u1_s1, u1_s2, recipe_1_s1):
|
||||||
|
with scopes_disabled():
|
||||||
|
i = recipe_1_s1.steps.first().ingredients.first()
|
||||||
|
r = u1_s2.delete(
|
||||||
|
reverse(
|
||||||
|
DETAIL_URL,
|
||||||
|
args={i.id}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert r.status_code == 404
|
||||||
|
|
||||||
|
r = u1_s1.delete(
|
||||||
|
reverse(
|
||||||
|
DETAIL_URL,
|
||||||
|
args={i.id}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert r.status_code == 204
|
||||||
|
assert not Ingredient.objects.filter(pk=i.id).exists()
|
@ -1,93 +1,148 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from cookbook.models import Keyword
|
import pytest
|
||||||
from cookbook.tests.views.test_views import TestViews
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
class TestApiKeyword(TestViews):
|
from cookbook.models import Keyword
|
||||||
|
|
||||||
def setUp(self):
|
LIST_URL = 'api:keyword-list'
|
||||||
super(TestApiKeyword, self).setUp()
|
DETAIL_URL = 'api:keyword-detail'
|
||||||
self.keyword_1 = Keyword.objects.create(
|
|
||||||
name='meat'
|
|
||||||
)
|
|
||||||
self.keyword_2 = Keyword.objects.create(
|
|
||||||
name='veggies'
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_keyword_list(self):
|
|
||||||
# verify view permissions are applied accordingly
|
|
||||||
self.batch_requests(
|
|
||||||
[
|
|
||||||
(self.anonymous_client, 403),
|
|
||||||
(self.guest_client_1, 403),
|
|
||||||
(self.user_client_1, 200),
|
|
||||||
(self.admin_client_1, 200),
|
|
||||||
(self.superuser_client, 200)
|
|
||||||
],
|
|
||||||
reverse('api:keyword-list')
|
|
||||||
)
|
|
||||||
|
|
||||||
# verify storage is returned
|
@pytest.fixture()
|
||||||
r = self.user_client_1.get(reverse('api:keyword-list'))
|
def obj_1(space_1):
|
||||||
self.assertEqual(r.status_code, 200)
|
return Keyword.objects.get_or_create(name='test_1', space=space_1)[0]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def obj_2(space_1):
|
||||||
|
return Keyword.objects.get_or_create(name='test_2', space=space_1)[0]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("arg", [
|
||||||
|
['a_u', 403],
|
||||||
|
['g1_s1', 403],
|
||||||
|
['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)) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_filter(obj_1, obj_2, u1_s1):
|
||||||
|
r = u1_s1.get(reverse(LIST_URL))
|
||||||
|
assert r.status_code == 200
|
||||||
response = json.loads(r.content)
|
response = json.loads(r.content)
|
||||||
self.assertEqual(len(response), 2)
|
assert len(response) == 2
|
||||||
self.assertEqual(response[0]['name'], self.keyword_1.name)
|
assert response[0]['name'] == obj_1.name
|
||||||
|
|
||||||
r = self.user_client_1.get(f'{reverse("api:keyword-list")}?limit=1')
|
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?limit=1').content)
|
||||||
response = json.loads(r.content)
|
assert len(response) == 1
|
||||||
self.assertEqual(len(response), 1)
|
|
||||||
|
|
||||||
r = self.user_client_1.get(
|
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query=chicken').content)
|
||||||
f'{reverse("api:keyword-list")}?query=chicken'
|
assert len(response) == 0
|
||||||
)
|
|
||||||
response = json.loads(r.content)
|
|
||||||
self.assertEqual(len(response), 0)
|
|
||||||
|
|
||||||
r = self.user_client_1.get(f'{reverse("api:keyword-list")}?query=MEAT')
|
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query={obj_1.name[4:]}').content)
|
||||||
response = json.loads(r.content)
|
assert len(response) == 1
|
||||||
self.assertEqual(len(response), 1)
|
|
||||||
|
|
||||||
def test_keyword_update(self):
|
|
||||||
r = self.user_client_1.patch(
|
@pytest.mark.parametrize("arg", [
|
||||||
|
['a_u', 403],
|
||||||
|
['g1_s1', 403],
|
||||||
|
['u1_s1', 200],
|
||||||
|
['a1_s1', 200],
|
||||||
|
['g1_s2', 403],
|
||||||
|
['u1_s2', 404],
|
||||||
|
['a1_s2', 404],
|
||||||
|
])
|
||||||
|
def test_update(arg, request, obj_1):
|
||||||
|
c = request.getfixturevalue(arg[0])
|
||||||
|
r = c.patch(
|
||||||
reverse(
|
reverse(
|
||||||
'api:keyword-detail',
|
DETAIL_URL,
|
||||||
args={self.keyword_1.id}
|
args={obj_1.id}
|
||||||
),
|
),
|
||||||
{'name': 'new'},
|
{'name': 'new'},
|
||||||
content_type='application/json'
|
content_type='application/json'
|
||||||
)
|
)
|
||||||
response = json.loads(r.content)
|
response = json.loads(r.content)
|
||||||
self.assertEqual(r.status_code, 200)
|
assert r.status_code == arg[1]
|
||||||
self.assertEqual(response['name'], 'new')
|
if r.status_code == 200:
|
||||||
|
assert response['name'] == 'new'
|
||||||
|
|
||||||
def test_keyword_add(self):
|
|
||||||
r = self.user_client_1.post(
|
@pytest.mark.parametrize("arg", [
|
||||||
reverse('api:keyword-list'),
|
['a_u', 403],
|
||||||
|
['g1_s1', 403],
|
||||||
|
['u1_s1', 201],
|
||||||
|
['a1_s1', 201],
|
||||||
|
])
|
||||||
|
def test_add(arg, request, u1_s2):
|
||||||
|
c = request.getfixturevalue(arg[0])
|
||||||
|
r = c.post(
|
||||||
|
reverse(LIST_URL),
|
||||||
{'name': 'test'},
|
{'name': 'test'},
|
||||||
content_type='application/json'
|
content_type='application/json'
|
||||||
)
|
)
|
||||||
response = json.loads(r.content)
|
response = json.loads(r.content)
|
||||||
self.assertEqual(r.status_code, 201)
|
assert r.status_code == arg[1]
|
||||||
self.assertEqual(response['name'], 'test')
|
if r.status_code == 201:
|
||||||
|
assert response['name'] == 'test'
|
||||||
|
r = c.get(reverse(DETAIL_URL, args={response['id']}))
|
||||||
|
assert r.status_code == 200
|
||||||
|
r = u1_s2.get(reverse(DETAIL_URL, args={response['id']}))
|
||||||
|
assert r.status_code == 404
|
||||||
|
|
||||||
def test_keyword_add_duplicate(self):
|
|
||||||
r = self.user_client_1.post(
|
def test_add_duplicate(u1_s1, u1_s2, obj_1):
|
||||||
reverse('api:keyword-list'),
|
r = u1_s1.post(
|
||||||
{'name': self.keyword_1.name},
|
reverse(LIST_URL),
|
||||||
|
{'name': obj_1.name},
|
||||||
content_type='application/json'
|
content_type='application/json'
|
||||||
)
|
)
|
||||||
response = json.loads(r.content)
|
response = json.loads(r.content)
|
||||||
self.assertEqual(r.status_code, 201)
|
assert r.status_code == 201
|
||||||
self.assertEqual(response['name'], self.keyword_1.name)
|
assert response['id'] == obj_1.id
|
||||||
|
|
||||||
def test_keyword_delete(self):
|
r = u1_s2.post(
|
||||||
r = self.user_client_1.delete(
|
reverse(LIST_URL),
|
||||||
|
{'name': obj_1.name},
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
response = json.loads(r.content)
|
||||||
|
assert r.status_code == 201
|
||||||
|
assert response['id'] != obj_1.id
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete(u1_s1, u1_s2, obj_1):
|
||||||
|
r = u1_s2.delete(
|
||||||
reverse(
|
reverse(
|
||||||
'api:keyword-detail',
|
DETAIL_URL,
|
||||||
args={self.keyword_1.id}
|
args={obj_1.id}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.assertEqual(r.status_code, 204)
|
assert r.status_code == 404
|
||||||
self.assertEqual(Keyword.objects.count(), 1)
|
|
||||||
|
r = u1_s1.delete(
|
||||||
|
reverse(
|
||||||
|
DETAIL_URL,
|
||||||
|
args={obj_1.id}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert r.status_code == 204
|
||||||
|
with scopes_disabled():
|
||||||
|
assert Keyword.objects.count() == 0
|
||||||
|
134
cookbook/tests/api/test_api_meal_plan.py
Normal file
134
cookbook/tests/api/test_api_meal_plan.py
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import json
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.contrib import auth
|
||||||
|
from django.urls import reverse
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
from cookbook.models import Food, MealPlan, MealType
|
||||||
|
|
||||||
|
LIST_URL = 'api:mealplan-list'
|
||||||
|
DETAIL_URL = 'api:mealplan-detail'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def meal_type(space_1, u1_s1):
|
||||||
|
return MealType.objects.get_or_create(name='test', space=space_1, created_by=auth.get_user(u1_s1))[0]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def obj_1(space_1, recipe_1_s1, meal_type, u1_s1):
|
||||||
|
return MealPlan.objects.create(recipe=recipe_1_s1, space=space_1, meal_type=meal_type, date=datetime.now(), created_by=auth.get_user(u1_s1))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def obj_2(space_1, recipe_1_s1, meal_type, u1_s1):
|
||||||
|
return MealPlan.objects.create(recipe=recipe_1_s1, space=space_1, meal_type=meal_type, date=datetime.now(), created_by=auth.get_user(u1_s1))
|
||||||
|
|
||||||
|
|
||||||
|
@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_list_filter(obj_1, u1_s1):
|
||||||
|
r = u1_s1.get(reverse(LIST_URL))
|
||||||
|
assert r.status_code == 200
|
||||||
|
response = json.loads(r.content)
|
||||||
|
assert len(response) == 1
|
||||||
|
|
||||||
|
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?from_date={(datetime.now() + timedelta(days=2)).strftime("%Y-%m-%d")}').content)
|
||||||
|
assert len(response) == 0
|
||||||
|
|
||||||
|
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?to_date={(datetime.now() - timedelta(days=2)).strftime("%Y-%m-%d")}').content)
|
||||||
|
assert len(response) == 0
|
||||||
|
|
||||||
|
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?from_date={(datetime.now() - timedelta(days=2)).strftime("%Y-%m-%d")}&to_date={(datetime.now() + timedelta(days=2)).strftime("%Y-%m-%d")}').content)
|
||||||
|
assert len(response) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@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}
|
||||||
|
),
|
||||||
|
{'title': 'new'},
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
response = json.loads(r.content)
|
||||||
|
assert r.status_code == arg[1]
|
||||||
|
if r.status_code == 200:
|
||||||
|
assert response['title'] == 'new'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("arg", [
|
||||||
|
['a_u', 403],
|
||||||
|
['g1_s1', 201],
|
||||||
|
['u1_s1', 201],
|
||||||
|
['a1_s1', 201],
|
||||||
|
])
|
||||||
|
def test_add(arg, request, u1_s2, recipe_1_s1, meal_type):
|
||||||
|
c = request.getfixturevalue(arg[0])
|
||||||
|
r = c.post(
|
||||||
|
reverse(LIST_URL),
|
||||||
|
{'recipe': recipe_1_s1.id, 'meal_type': meal_type.id, 'date': (datetime.now()).strftime("%Y-%m-%d"), 'servings': 1, 'title': 'test'},
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
response = json.loads(r.content)
|
||||||
|
assert r.status_code == arg[1]
|
||||||
|
if r.status_code == 201:
|
||||||
|
assert response['title'] == 'test'
|
||||||
|
r = c.get(reverse(DETAIL_URL, args={response['id']}))
|
||||||
|
assert r.status_code == 200
|
||||||
|
r = u1_s2.get(reverse(DETAIL_URL, args={response['id']}))
|
||||||
|
assert r.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
with scopes_disabled():
|
||||||
|
assert MealPlan.objects.count() == 0
|
132
cookbook/tests/api/test_api_meal_type.py
Normal file
132
cookbook/tests/api/test_api_meal_type.py
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
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, MealType
|
||||||
|
|
||||||
|
LIST_URL = 'api:mealtype-list'
|
||||||
|
DETAIL_URL = 'api:mealtype-detail'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def obj_1(space_1, u1_s1):
|
||||||
|
return MealType.objects.get_or_create(name='test_1', created_by=auth.get_user(u1_s1), space=space_1)[0]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def obj_2(space_1, u1_s1):
|
||||||
|
return MealType.objects.get_or_create(name='test_2', created_by=auth.get_user(u1_s1), space=space_1)[0]
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
|
||||||
|
@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}
|
||||||
|
),
|
||||||
|
{'name': 'new'},
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
response = json.loads(r.content)
|
||||||
|
assert r.status_code == arg[1]
|
||||||
|
if r.status_code == 200:
|
||||||
|
assert response['name'] == 'new'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("arg", [
|
||||||
|
['a_u', 403],
|
||||||
|
['g1_s1', 201],
|
||||||
|
['u1_s1', 201],
|
||||||
|
['a1_s1', 201],
|
||||||
|
])
|
||||||
|
def test_add(arg, request, u1_s2):
|
||||||
|
c = request.getfixturevalue(arg[0])
|
||||||
|
r = c.post(
|
||||||
|
reverse(LIST_URL),
|
||||||
|
{'name': 'test'},
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
response = json.loads(r.content)
|
||||||
|
assert r.status_code == arg[1]
|
||||||
|
if r.status_code == 201:
|
||||||
|
assert response['name'] == 'test'
|
||||||
|
r = c.get(reverse(DETAIL_URL, args={response['id']}))
|
||||||
|
assert r.status_code == 200
|
||||||
|
r = u1_s2.get(reverse(DETAIL_URL, args={response['id']}))
|
||||||
|
assert r.status_code == 404
|
||||||
|
|
||||||
|
# TODO make name in space unique
|
||||||
|
# def test_add_duplicate(u1_s1, u1_s2, obj_1):
|
||||||
|
# r = u1_s1.post(
|
||||||
|
# reverse(LIST_URL),
|
||||||
|
# {'name': obj_1.name},
|
||||||
|
# content_type='application/json'
|
||||||
|
# )
|
||||||
|
# response = json.loads(r.content)
|
||||||
|
# assert r.status_code == 201
|
||||||
|
# assert response['id'] == obj_1.id
|
||||||
|
#
|
||||||
|
# r = u1_s2.post(
|
||||||
|
# reverse(LIST_URL),
|
||||||
|
# {'name': obj_1.name},
|
||||||
|
# content_type='application/json'
|
||||||
|
# )
|
||||||
|
# response = json.loads(r.content)
|
||||||
|
# assert r.status_code == 201
|
||||||
|
# assert response['id'] != obj_1.id
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
with scopes_disabled():
|
||||||
|
assert MealType.objects.count() == 0
|
@ -1,31 +1,104 @@
|
|||||||
from cookbook.models import Recipe
|
import json
|
||||||
from cookbook.tests.views.test_views import TestViews
|
|
||||||
from django.contrib import auth
|
import pytest
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
from cookbook.models import Food, Ingredient, Step, Recipe
|
||||||
|
|
||||||
|
LIST_URL = 'api:recipe-list'
|
||||||
|
DETAIL_URL = 'api:recipe-detail'
|
||||||
|
|
||||||
|
|
||||||
class TestApiShopping(TestViews):
|
@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 setUp(self):
|
|
||||||
super(TestApiShopping, self).setUp()
|
|
||||||
self.internal_recipe = Recipe.objects.create(
|
|
||||||
name='Test',
|
|
||||||
internal=True,
|
|
||||||
created_by=auth.get_user(self.user_client_1)
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_shopping_view_permissions(self):
|
def test_list_space(recipe_1_s1, u1_s1, u1_s2, space_2):
|
||||||
self.batch_requests(
|
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 1
|
||||||
[
|
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0
|
||||||
(self.anonymous_client, 403),
|
|
||||||
(self.guest_client_1, 200),
|
with scopes_disabled():
|
||||||
(self.user_client_1, 200),
|
recipe_1_s1.space = space_2
|
||||||
(self.user_client_2, 200),
|
recipe_1_s1.save()
|
||||||
(self.admin_client_1, 200),
|
|
||||||
(self.superuser_client, 200)
|
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 0
|
||||||
],
|
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("arg", [
|
||||||
|
['a_u', 403],
|
||||||
|
['g1_s1', 200],
|
||||||
|
['u1_s1', 200],
|
||||||
|
['a1_s1', 200],
|
||||||
|
['g1_s2', 404],
|
||||||
|
['u1_s2', 404],
|
||||||
|
['a1_s2', 404],
|
||||||
|
])
|
||||||
|
def test_update(arg, request, recipe_1_s1):
|
||||||
|
with scopes_disabled():
|
||||||
|
c = request.getfixturevalue(arg[0])
|
||||||
|
r = c.patch(
|
||||||
reverse(
|
reverse(
|
||||||
'api:recipe-detail', args={self.internal_recipe.id})
|
DETAIL_URL,
|
||||||
|
args={recipe_1_s1.id}
|
||||||
|
),
|
||||||
|
{'name': 'new'},
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
response = json.loads(r.content)
|
||||||
|
assert r.status_code == arg[1]
|
||||||
|
if r.status_code == 200:
|
||||||
|
assert response['name'] == 'new'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("arg", [
|
||||||
|
['a_u', 403],
|
||||||
|
['g1_s1', 201],
|
||||||
|
['u1_s1', 201],
|
||||||
|
['a1_s1', 201],
|
||||||
|
])
|
||||||
|
def test_add(arg, request, u1_s2):
|
||||||
|
c = request.getfixturevalue(arg[0])
|
||||||
|
r = c.post(
|
||||||
|
reverse(LIST_URL),
|
||||||
|
{'name': 'test', 'waiting_time': 0, 'working_time': 0, 'keywords': [], 'steps': []},
|
||||||
|
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['id'] == 1
|
||||||
|
r = c.get(reverse(DETAIL_URL, args={response['id']}))
|
||||||
|
assert r.status_code == 200
|
||||||
|
r = u1_s2.get(reverse(DETAIL_URL, args={response['id']}))
|
||||||
|
assert r.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete(u1_s1, u1_s2, recipe_1_s1):
|
||||||
|
with scopes_disabled():
|
||||||
|
r = u1_s2.delete(
|
||||||
|
reverse(
|
||||||
|
DETAIL_URL,
|
||||||
|
args={recipe_1_s1.id}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert r.status_code == 404
|
||||||
|
|
||||||
|
r = u1_s1.delete(
|
||||||
|
reverse(
|
||||||
|
DETAIL_URL,
|
||||||
|
args={recipe_1_s1.id}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO add tests for editing
|
assert r.status_code == 204
|
||||||
|
assert not Recipe.objects.filter(pk=recipe_1_s1.id).exists()
|
||||||
|
130
cookbook/tests/api/test_api_recipe_book.py
Normal file
130
cookbook/tests/api/test_api_recipe_book.py
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.contrib import auth
|
||||||
|
from django.urls import reverse
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
from cookbook.models import RecipeBook
|
||||||
|
|
||||||
|
LIST_URL = 'api:recipebook-list'
|
||||||
|
DETAIL_URL = 'api:recipebook-detail'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def obj_1(space_1, u1_s1):
|
||||||
|
return RecipeBook.objects.get_or_create(name='test_1', created_by=auth.get_user(u1_s1), space=space_1)[0]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def obj_2(space_1, u1_s1):
|
||||||
|
return RecipeBook.objects.get_or_create(name='test_2', created_by=auth.get_user(u1_s1), space=space_1)[0]
|
||||||
|
|
||||||
|
|
||||||
|
@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_list_filter(obj_1, obj_2, u1_s1):
|
||||||
|
r = u1_s1.get(reverse(LIST_URL))
|
||||||
|
assert r.status_code == 200
|
||||||
|
response = json.loads(r.content)
|
||||||
|
assert len(response) == 2
|
||||||
|
assert response[0]['name'] == obj_1.name
|
||||||
|
|
||||||
|
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?limit=1').content)
|
||||||
|
assert len(response) == 1
|
||||||
|
|
||||||
|
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query=chicken').content)
|
||||||
|
assert len(response) == 0
|
||||||
|
|
||||||
|
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query={obj_1.name[4:]}').content)
|
||||||
|
assert len(response) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@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}
|
||||||
|
),
|
||||||
|
{'name': 'new'},
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
response = json.loads(r.content)
|
||||||
|
assert r.status_code == arg[1]
|
||||||
|
if r.status_code == 200:
|
||||||
|
assert response['name'] == 'new'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("arg", [
|
||||||
|
['a_u', 403],
|
||||||
|
['g1_s1', 201],
|
||||||
|
['u1_s1', 201],
|
||||||
|
['a1_s1', 201],
|
||||||
|
])
|
||||||
|
def test_add(arg, request, u1_s2):
|
||||||
|
c = request.getfixturevalue(arg[0])
|
||||||
|
r = c.post(
|
||||||
|
reverse(LIST_URL),
|
||||||
|
{'name': 'test'},
|
||||||
|
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['name'] == 'test'
|
||||||
|
r = c.get(reverse(DETAIL_URL, args={response['id']}))
|
||||||
|
assert r.status_code == 200
|
||||||
|
r = u1_s2.get(reverse(DETAIL_URL, args={response['id']}))
|
||||||
|
assert r.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
with scopes_disabled():
|
||||||
|
assert RecipeBook.objects.count() == 0
|
124
cookbook/tests/api/test_api_recipe_book_entry.py
Normal file
124
cookbook/tests/api/test_api_recipe_book_entry.py
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.contrib import auth
|
||||||
|
from django.urls import reverse
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
from cookbook.models import RecipeBook, RecipeBookEntry
|
||||||
|
|
||||||
|
LIST_URL = 'api:recipebookentry-list'
|
||||||
|
DETAIL_URL = 'api:recipebookentry-detail'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def obj_1(space_1, u1_s1, recipe_1_s1):
|
||||||
|
b = RecipeBook.objects.create(name='test_1', created_by=auth.get_user(u1_s1), space=space_1)
|
||||||
|
|
||||||
|
return RecipeBookEntry.objects.create(book=b, recipe=recipe_1_s1)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def obj_2(space_1, u1_s1, recipe_1_s1):
|
||||||
|
b = RecipeBook.objects.create(name='test_1', created_by=auth.get_user(u1_s1), space=space_1)
|
||||||
|
return RecipeBookEntry.objects.create(book=b, recipe=recipe_1_s1)
|
||||||
|
|
||||||
|
|
||||||
|
@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.book.space = space_2
|
||||||
|
obj_1.book.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, recipe_2_s1):
|
||||||
|
c = request.getfixturevalue(arg[0])
|
||||||
|
r = c.patch(
|
||||||
|
reverse(
|
||||||
|
DETAIL_URL,
|
||||||
|
args={obj_1.id}
|
||||||
|
),
|
||||||
|
{'recipe': recipe_2_s1.pk},
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
response = json.loads(r.content)
|
||||||
|
assert r.status_code == arg[1]
|
||||||
|
if r.status_code == 200:
|
||||||
|
assert response['recipe'] == recipe_2_s1.pk
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("arg", [
|
||||||
|
['a_u', 403],
|
||||||
|
['g1_s1', 404],
|
||||||
|
['u1_s1', 201],
|
||||||
|
['a1_s1', 404],
|
||||||
|
])
|
||||||
|
def test_add(arg, request, u1_s2, obj_1, recipe_2_s1):
|
||||||
|
c = request.getfixturevalue(arg[0])
|
||||||
|
r = c.post(
|
||||||
|
reverse(LIST_URL),
|
||||||
|
{'book': obj_1.book.pk, 'recipe': recipe_2_s1.pk},
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
response = json.loads(r.content)
|
||||||
|
assert r.status_code == arg[1]
|
||||||
|
if r.status_code == 201:
|
||||||
|
assert response['recipe'] == recipe_2_s1.pk
|
||||||
|
r = c.get(reverse(DETAIL_URL, args={response['id']}))
|
||||||
|
assert r.status_code == 200
|
||||||
|
r = u1_s2.get(reverse(DETAIL_URL, args={response['id']}))
|
||||||
|
assert r.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_duplicate(u1_s1, obj_1):
|
||||||
|
r = u1_s1.post(
|
||||||
|
reverse(LIST_URL),
|
||||||
|
{'book': obj_1.book.pk, 'recipe': obj_1.recipe.pk},
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
assert r.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
with scopes_disabled():
|
||||||
|
assert RecipeBookEntry.objects.count() == 0
|
@ -1,48 +0,0 @@
|
|||||||
from cookbook.models import ShoppingList
|
|
||||||
from cookbook.tests.views.test_views import TestViews
|
|
||||||
from django.contrib import auth
|
|
||||||
from django.urls import reverse
|
|
||||||
|
|
||||||
|
|
||||||
class TestApiShopping(TestViews):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(TestApiShopping, self).setUp()
|
|
||||||
self.list_1 = ShoppingList.objects.create(
|
|
||||||
created_by=auth.get_user(self.user_client_1)
|
|
||||||
)
|
|
||||||
self.list_2 = ShoppingList.objects.create(
|
|
||||||
created_by=auth.get_user(self.user_client_2)
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_shopping_view_permissions(self):
|
|
||||||
self.batch_requests(
|
|
||||||
[
|
|
||||||
(self.anonymous_client, 403),
|
|
||||||
(self.guest_client_1, 404),
|
|
||||||
(self.user_client_1, 200),
|
|
||||||
(self.user_client_2, 404),
|
|
||||||
(self.admin_client_1, 404),
|
|
||||||
(self.superuser_client, 200)
|
|
||||||
],
|
|
||||||
reverse(
|
|
||||||
'api:shoppinglist-detail', args={self.list_1.id}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.list_1.shared.add(auth.get_user(self.user_client_2))
|
|
||||||
|
|
||||||
self.batch_requests(
|
|
||||||
[
|
|
||||||
(self.anonymous_client, 403),
|
|
||||||
(self.guest_client_1, 404),
|
|
||||||
(self.user_client_1, 200),
|
|
||||||
(self.user_client_2, 200),
|
|
||||||
(self.admin_client_1, 404),
|
|
||||||
(self.superuser_client, 200)
|
|
||||||
],
|
|
||||||
reverse(
|
|
||||||
'api:shoppinglist-detail', args={self.list_1.id})
|
|
||||||
)
|
|
||||||
|
|
||||||
# TODO add tests for editing
|
|
120
cookbook/tests/api/test_api_shopping_list.py
Normal file
120
cookbook/tests/api/test_api_shopping_list.py
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.contrib import auth
|
||||||
|
from django.urls import reverse
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
from cookbook.models import RecipeBook, Storage, Sync, SyncLog, 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
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
116
cookbook/tests/api/test_api_shopping_list_entry.py
Normal file
116
cookbook/tests/api/test_api_shopping_list_entry.py
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.contrib import auth
|
||||||
|
from django.forms import model_to_dict
|
||||||
|
from django.urls import reverse
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
from cookbook.models import RecipeBook, Storage, Sync, SyncLog, ShoppingList, ShoppingListEntry, Food
|
||||||
|
|
||||||
|
LIST_URL = 'api:shoppinglistentry-list'
|
||||||
|
DETAIL_URL = 'api:shoppinglistentry-detail'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def obj_1(space_1, u1_s1):
|
||||||
|
e = ShoppingListEntry.objects.create(food=Food.objects.create(name='test 1', 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(food=Food.objects.create(name='test 2', 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()
|
||||||
|
s.space = space_2
|
||||||
|
s.save()
|
||||||
|
|
||||||
|
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 1
|
||||||
|
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("arg", [
|
||||||
|
['a_u', 403],
|
||||||
|
['g1_s1', 404],
|
||||||
|
['u1_s1', 200],
|
||||||
|
['a1_s1', 404],
|
||||||
|
['g1_s2', 404],
|
||||||
|
['u1_s2', 404],
|
||||||
|
['a1_s2', 404],
|
||||||
|
])
|
||||||
|
def test_update(arg, request, obj_1):
|
||||||
|
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', 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': model_to_dict(obj_1.food), '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
|
116
cookbook/tests/api/test_api_shopping_list_recipe.py
Normal file
116
cookbook/tests/api/test_api_shopping_list_recipe.py
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.contrib import auth
|
||||||
|
from django.forms import model_to_dict
|
||||||
|
from django.urls import reverse
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
from cookbook.models import RecipeBook, Storage, Sync, SyncLog, ShoppingList, ShoppingListEntry, Food, ShoppingListRecipe
|
||||||
|
|
||||||
|
LIST_URL = 'api:shoppinglistrecipe-list'
|
||||||
|
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)
|
||||||
|
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],
|
||||||
|
])
|
||||||
|
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],
|
||||||
|
])
|
||||||
|
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'
|
||||||
|
)
|
||||||
|
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],
|
||||||
|
])
|
||||||
|
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'
|
||||||
|
)
|
||||||
|
response = json.loads(r.content)
|
||||||
|
print(r.content)
|
||||||
|
assert r.status_code == arg[1]
|
||||||
|
if r.status_code == 201:
|
||||||
|
assert response['recipe'] == recipe_1_s1.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
|
106
cookbook/tests/api/test_api_step.py
Normal file
106
cookbook/tests/api/test_api_step.py
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.urls import reverse
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
from cookbook.models import Food, Ingredient, Step
|
||||||
|
|
||||||
|
LIST_URL = 'api:step-list'
|
||||||
|
DETAIL_URL = 'api:step-detail'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("arg", [
|
||||||
|
['a_u', 403],
|
||||||
|
['g1_s1', 403],
|
||||||
|
['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(recipe_1_s1, 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():
|
||||||
|
recipe_1_s1.space = space_2
|
||||||
|
recipe_1_s1.save()
|
||||||
|
|
||||||
|
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 0
|
||||||
|
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("arg", [
|
||||||
|
['a_u', 403],
|
||||||
|
['g1_s1', 403],
|
||||||
|
['u1_s1', 200],
|
||||||
|
['a1_s1', 200],
|
||||||
|
['g1_s2', 403],
|
||||||
|
['u1_s2', 404],
|
||||||
|
['a1_s2', 404],
|
||||||
|
])
|
||||||
|
def test_update(arg, request, recipe_1_s1):
|
||||||
|
with scopes_disabled():
|
||||||
|
s = recipe_1_s1.steps.first()
|
||||||
|
c = request.getfixturevalue(arg[0])
|
||||||
|
r = c.patch(
|
||||||
|
reverse(
|
||||||
|
DETAIL_URL,
|
||||||
|
args={s.id}
|
||||||
|
),
|
||||||
|
{'instruction': 'new'},
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
response = json.loads(r.content)
|
||||||
|
assert r.status_code == arg[1]
|
||||||
|
if r.status_code == 200:
|
||||||
|
assert response['instruction'] == 'new'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("arg", [
|
||||||
|
['a_u', 403],
|
||||||
|
['g1_s1', 403],
|
||||||
|
['u1_s1', 201],
|
||||||
|
['a1_s1', 201],
|
||||||
|
])
|
||||||
|
def test_add(arg, request, u1_s2):
|
||||||
|
c = request.getfixturevalue(arg[0])
|
||||||
|
r = c.post(
|
||||||
|
reverse(LIST_URL),
|
||||||
|
{'instruction': 'test', 'ingredients': []},
|
||||||
|
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['id'] == 1
|
||||||
|
r = c.get(reverse(DETAIL_URL, args={response['id']}))
|
||||||
|
assert r.status_code == 404 # ingredient is not linked to a recipe and therefore cannot be accessed
|
||||||
|
r = u1_s2.get(reverse(DETAIL_URL, args={response['id']}))
|
||||||
|
assert r.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete(u1_s1, u1_s2, recipe_1_s1):
|
||||||
|
with scopes_disabled():
|
||||||
|
s = recipe_1_s1.steps.first()
|
||||||
|
r = u1_s2.delete(
|
||||||
|
reverse(
|
||||||
|
DETAIL_URL,
|
||||||
|
args={s.id}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert r.status_code == 404
|
||||||
|
|
||||||
|
r = u1_s1.delete(
|
||||||
|
reverse(
|
||||||
|
DETAIL_URL,
|
||||||
|
args={s.id}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert r.status_code == 204
|
||||||
|
assert not Step.objects.filter(pk=s.id).exists()
|
@ -1,82 +1,122 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from cookbook.models import Storage, Sync
|
import pytest
|
||||||
from cookbook.tests.views.test_views import TestViews
|
|
||||||
from django.contrib import auth
|
from django.contrib import auth
|
||||||
from django.db.models import ProtectedError
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
from cookbook.models import RecipeBook, Storage, Sync
|
||||||
|
|
||||||
|
LIST_URL = 'api:storage-list'
|
||||||
|
DETAIL_URL = 'api:storage-detail'
|
||||||
|
|
||||||
|
|
||||||
class TestApiStorage(TestViews):
|
@pytest.fixture()
|
||||||
|
def obj_1(space_1, u1_s1):
|
||||||
|
return Storage.objects.create(name='Test Storage 1', username='test', password='password', token='token', url='url', created_by=auth.get_user(u1_s1), space=space_1, )
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(TestApiStorage, self).setUp()
|
|
||||||
self.storage = Storage.objects.create(
|
|
||||||
name='Test Storage',
|
|
||||||
username='test',
|
|
||||||
password='password',
|
|
||||||
token='token',
|
|
||||||
url='url',
|
|
||||||
created_by=auth.get_user(self.admin_client_1)
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_storage_list(self):
|
@pytest.fixture
|
||||||
# verify view permissions are applied accordingly
|
def obj_2(space_1, u1_s1):
|
||||||
self.batch_requests(
|
return Storage.objects.create(name='Test Storage 2', username='test', password='password', token='token', url='url', created_by=auth.get_user(u1_s1), space=space_1, )
|
||||||
[
|
|
||||||
(self.anonymous_client, 403),
|
|
||||||
(self.guest_client_1, 403),
|
|
||||||
(self.user_client_1, 403),
|
|
||||||
(self.admin_client_1, 200),
|
|
||||||
(self.superuser_client, 200)
|
|
||||||
],
|
|
||||||
reverse('api:storage-list')
|
|
||||||
)
|
|
||||||
|
|
||||||
# verify storage is returned
|
|
||||||
r = self.admin_client_1.get(reverse('api:storage-list'))
|
@pytest.mark.parametrize("arg", [
|
||||||
self.assertEqual(r.status_code, 200)
|
['a_u', 403],
|
||||||
|
['g1_s1', 403],
|
||||||
|
['u1_s1', 403],
|
||||||
|
['a1_s1', 200],
|
||||||
|
])
|
||||||
|
def test_list_permission(arg, request):
|
||||||
|
c = request.getfixturevalue(arg[0])
|
||||||
|
r = c.get(reverse(LIST_URL))
|
||||||
|
assert r.status_code == arg[1]
|
||||||
|
if r.status_code == 200:
|
||||||
response = json.loads(r.content)
|
response = json.loads(r.content)
|
||||||
self.assertEqual(len(response), 1)
|
assert 'password' not in response
|
||||||
storage_response = response[0]
|
assert 'token' not in response
|
||||||
self.assertEqual(storage_response['name'], self.storage.name)
|
|
||||||
self.assertFalse('password' in storage_response)
|
|
||||||
self.assertFalse('token' in storage_response)
|
|
||||||
|
|
||||||
def test_storage_update(self):
|
|
||||||
# can update storage as admin
|
def test_list_space(obj_1, obj_2, a1_s1, a1_s2, space_2):
|
||||||
r = self.admin_client_1.patch(
|
assert len(json.loads(a1_s1.get(reverse(LIST_URL)).content)) == 2
|
||||||
|
assert len(json.loads(a1_s2.get(reverse(LIST_URL)).content)) == 0
|
||||||
|
|
||||||
|
obj_1.space = space_2
|
||||||
|
obj_1.save()
|
||||||
|
|
||||||
|
assert len(json.loads(a1_s1.get(reverse(LIST_URL)).content)) == 1
|
||||||
|
assert len(json.loads(a1_s2.get(reverse(LIST_URL)).content)) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("arg", [
|
||||||
|
['a_u', 403],
|
||||||
|
['g1_s1', 403],
|
||||||
|
['u1_s1', 403],
|
||||||
|
['a1_s1', 200],
|
||||||
|
['g1_s2', 403],
|
||||||
|
['u1_s2', 403],
|
||||||
|
['a1_s2', 404],
|
||||||
|
])
|
||||||
|
def test_update(arg, request, obj_1):
|
||||||
|
test_password = '1234'
|
||||||
|
|
||||||
|
c = request.getfixturevalue(arg[0])
|
||||||
|
r = c.patch(
|
||||||
reverse(
|
reverse(
|
||||||
'api:storage-detail',
|
DETAIL_URL,
|
||||||
args={self.storage.id}
|
args={obj_1.id}
|
||||||
),
|
),
|
||||||
{'name': 'new', 'password': 'new_password'},
|
{'name': 'new', 'password': test_password},
|
||||||
content_type='application/json'
|
content_type='application/json'
|
||||||
)
|
)
|
||||||
response = json.loads(r.content)
|
response = json.loads(r.content)
|
||||||
self.assertEqual(r.status_code, 200)
|
assert r.status_code == arg[1]
|
||||||
self.assertEqual(response['name'], 'new')
|
if r.status_code == 200:
|
||||||
|
assert response['name'] == 'new'
|
||||||
|
obj_1.refresh_from_db()
|
||||||
|
assert obj_1.password == test_password
|
||||||
|
|
||||||
# verify password was updated (write only field)
|
|
||||||
self.storage.refresh_from_db()
|
|
||||||
self.assertEqual(self.storage.password, 'new_password')
|
|
||||||
|
|
||||||
def test_storage_delete(self):
|
@pytest.mark.parametrize("arg", [
|
||||||
# can delete storage as admin
|
['a_u', 403],
|
||||||
r = self.admin_client_1.delete(
|
['g1_s1', 403],
|
||||||
reverse('api:storage-detail', args={self.storage.id})
|
['u1_s1', 403],
|
||||||
|
['a1_s1', 201],
|
||||||
|
])
|
||||||
|
def test_add(arg, request, a1_s2, obj_1):
|
||||||
|
c = request.getfixturevalue(arg[0])
|
||||||
|
r = c.post(
|
||||||
|
reverse(LIST_URL),
|
||||||
|
{'name': 'test', 'method': Storage.DROPBOX},
|
||||||
|
content_type='application/json'
|
||||||
)
|
)
|
||||||
self.assertEqual(r.status_code, 204)
|
response = json.loads(r.content)
|
||||||
self.assertEqual(Storage.objects.count(), 0)
|
print(r.content)
|
||||||
|
assert r.status_code == arg[1]
|
||||||
|
if r.status_code == 201:
|
||||||
|
assert response['name'] == 'test'
|
||||||
|
r = c.get(reverse(DETAIL_URL, args={response['id']}))
|
||||||
|
assert r.status_code == 200
|
||||||
|
r = a1_s2.get(reverse(DETAIL_URL, args={response['id']}))
|
||||||
|
assert r.status_code == 404
|
||||||
|
|
||||||
self.storage = Storage.objects.create(
|
|
||||||
created_by=auth.get_user(self.admin_client_1), name='test protect'
|
|
||||||
)
|
|
||||||
Sync.objects.create(storage=self.storage, )
|
|
||||||
|
|
||||||
# test if deleting a storage with existing
|
def test_delete(a1_s1, a1_s2, obj_1):
|
||||||
# sync fails (as sync protects storage)
|
r = a1_s2.delete(
|
||||||
with self.assertRaises(ProtectedError):
|
reverse(
|
||||||
self.admin_client_1.delete(
|
DETAIL_URL,
|
||||||
reverse('api:storage-detail', args={self.storage.id})
|
args={obj_1.id}
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
assert r.status_code == 404
|
||||||
|
|
||||||
|
r = a1_s1.delete(
|
||||||
|
reverse(
|
||||||
|
DETAIL_URL,
|
||||||
|
args={obj_1.id}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert r.status_code == 204
|
||||||
|
with scopes_disabled():
|
||||||
|
assert Storage.objects.count() == 0
|
||||||
|
130
cookbook/tests/api/test_api_supermarket.py
Normal file
130
cookbook/tests/api/test_api_supermarket.py
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.contrib import auth
|
||||||
|
from django.urls import reverse
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
from cookbook.models import RecipeBook, Supermarket
|
||||||
|
|
||||||
|
LIST_URL = 'api:supermarket-list'
|
||||||
|
DETAIL_URL = 'api:supermarket-detail'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def obj_1(space_1, u1_s1):
|
||||||
|
return Supermarket.objects.get_or_create(name='test_1', space=space_1)[0]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def obj_2(space_1, u1_s1):
|
||||||
|
return Supermarket.objects.get_or_create(name='test_2', space=space_1)[0]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("arg", [
|
||||||
|
['a_u', 403],
|
||||||
|
['g1_s1', 403],
|
||||||
|
['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)) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_filter(obj_1, obj_2, u1_s1):
|
||||||
|
r = u1_s1.get(reverse(LIST_URL))
|
||||||
|
assert r.status_code == 200
|
||||||
|
response = json.loads(r.content)
|
||||||
|
assert len(response) == 2
|
||||||
|
assert response[0]['name'] == obj_1.name
|
||||||
|
|
||||||
|
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?limit=1').content)
|
||||||
|
assert len(response) == 1
|
||||||
|
|
||||||
|
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query=chicken').content)
|
||||||
|
assert len(response) == 0
|
||||||
|
|
||||||
|
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query={obj_1.name[4:]}').content)
|
||||||
|
assert len(response) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("arg", [
|
||||||
|
['a_u', 403],
|
||||||
|
['g1_s1', 403],
|
||||||
|
['u1_s1', 200],
|
||||||
|
['a1_s1', 200],
|
||||||
|
['g1_s2', 403],
|
||||||
|
['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}
|
||||||
|
),
|
||||||
|
{'name': 'new'},
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
response = json.loads(r.content)
|
||||||
|
assert r.status_code == arg[1]
|
||||||
|
if r.status_code == 200:
|
||||||
|
assert response['name'] == 'new'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("arg", [
|
||||||
|
['a_u', 403],
|
||||||
|
['g1_s1', 403],
|
||||||
|
['u1_s1', 201],
|
||||||
|
['a1_s1', 201],
|
||||||
|
])
|
||||||
|
def test_add(arg, request, u1_s2):
|
||||||
|
c = request.getfixturevalue(arg[0])
|
||||||
|
r = c.post(
|
||||||
|
reverse(LIST_URL),
|
||||||
|
{'name': 'test'},
|
||||||
|
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['name'] == 'test'
|
||||||
|
r = c.get(reverse(DETAIL_URL, args={response['id']}))
|
||||||
|
assert r.status_code == 200
|
||||||
|
r = u1_s2.get(reverse(DETAIL_URL, args={response['id']}))
|
||||||
|
assert r.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
with scopes_disabled():
|
||||||
|
assert Supermarket.objects.count() == 0
|
@ -1,70 +0,0 @@
|
|||||||
import json
|
|
||||||
|
|
||||||
from cookbook.models import Storage, Sync, SyncLog
|
|
||||||
from cookbook.tests.views.test_views import TestViews
|
|
||||||
from django.contrib import auth
|
|
||||||
from django.urls import reverse
|
|
||||||
|
|
||||||
|
|
||||||
class TestApiSyncLog(TestViews):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(TestApiSyncLog, self).setUp()
|
|
||||||
self.storage = Storage.objects.create(
|
|
||||||
name='Test Storage',
|
|
||||||
username='test',
|
|
||||||
password='password',
|
|
||||||
token='token',
|
|
||||||
url='url',
|
|
||||||
created_by=auth.get_user(self.admin_client_1)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.sync = Sync.objects.create(
|
|
||||||
storage=self.storage,
|
|
||||||
path='path'
|
|
||||||
)
|
|
||||||
|
|
||||||
self.sync_log = SyncLog.objects.create(
|
|
||||||
sync=self.sync, status='success'
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_sync_log_list(self):
|
|
||||||
# verify view permissions are applied accordingly
|
|
||||||
self.batch_requests(
|
|
||||||
[
|
|
||||||
(self.anonymous_client, 403),
|
|
||||||
(self.guest_client_1, 403),
|
|
||||||
(self.user_client_1, 403),
|
|
||||||
(self.admin_client_1, 200),
|
|
||||||
(self.superuser_client, 200)
|
|
||||||
],
|
|
||||||
reverse('api:synclog-list')
|
|
||||||
)
|
|
||||||
|
|
||||||
# verify log entry is returned
|
|
||||||
r = self.admin_client_1.get(reverse('api:synclog-list'))
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
response = json.loads(r.content)
|
|
||||||
self.assertEqual(len(response), 1)
|
|
||||||
self.assertEqual(response[0]['status'], self.sync_log.status)
|
|
||||||
|
|
||||||
def test_sync_log_update(self):
|
|
||||||
# read only view
|
|
||||||
r = self.admin_client_1.patch(
|
|
||||||
reverse(
|
|
||||||
'api:synclog-detail',
|
|
||||||
args={self.sync.id}
|
|
||||||
),
|
|
||||||
{'path': 'new'},
|
|
||||||
content_type='application/json'
|
|
||||||
)
|
|
||||||
self.assertEqual(r.status_code, 405)
|
|
||||||
|
|
||||||
def test_sync_log_delete(self):
|
|
||||||
# read only view
|
|
||||||
r = self.admin_client_1.delete(
|
|
||||||
reverse(
|
|
||||||
'api:synclog-detail',
|
|
||||||
args={self.sync.id})
|
|
||||||
)
|
|
||||||
self.assertEqual(r.status_code, 405)
|
|
@ -1,68 +1,115 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from cookbook.models import Storage, Sync
|
import pytest
|
||||||
from cookbook.tests.views.test_views import TestViews
|
|
||||||
from django.contrib import auth
|
from django.contrib import auth
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
from cookbook.models import RecipeBook, Storage, Sync
|
||||||
|
|
||||||
|
LIST_URL = 'api:sync-list'
|
||||||
|
DETAIL_URL = 'api:sync-detail'
|
||||||
|
|
||||||
|
|
||||||
class TestApiSync(TestViews):
|
@pytest.fixture()
|
||||||
|
def obj_1(space_1, u1_s1):
|
||||||
|
s = Storage.objects.create(name='Test Storage', username='test', password='password', token='token', url='url', created_by=auth.get_user(u1_s1), space=space_1, )
|
||||||
|
return Sync.objects.create(storage=s, path='path', space=space_1, )
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(TestApiSync, self).setUp()
|
|
||||||
self.storage = Storage.objects.create(
|
|
||||||
name='Test Storage',
|
|
||||||
username='test',
|
|
||||||
password='password',
|
|
||||||
token='token',
|
|
||||||
url='url',
|
|
||||||
created_by=auth.get_user(self.admin_client_1)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.sync = Sync.objects.create(
|
@pytest.fixture
|
||||||
storage=self.storage,
|
def obj_2(space_1, u1_s1):
|
||||||
path='path'
|
s = Storage.objects.create(name='Test Storage', username='test', password='password', token='token', url='url', created_by=auth.get_user(u1_s1), space=space_1, )
|
||||||
)
|
return Sync.objects.create(storage=s, path='path', space=space_1, )
|
||||||
|
|
||||||
def test_sync_list(self):
|
|
||||||
# verify view permissions are applied accordingly
|
|
||||||
self.batch_requests(
|
|
||||||
[
|
|
||||||
(self.anonymous_client, 403),
|
|
||||||
(self.guest_client_1, 403),
|
|
||||||
(self.user_client_1, 403),
|
|
||||||
(self.admin_client_1, 200),
|
|
||||||
(self.superuser_client, 200)
|
|
||||||
],
|
|
||||||
reverse('api:sync-list')
|
|
||||||
)
|
|
||||||
|
|
||||||
# verify sync is returned
|
@pytest.mark.parametrize("arg", [
|
||||||
r = self.admin_client_1.get(reverse('api:sync-list'))
|
['a_u', 403],
|
||||||
self.assertEqual(r.status_code, 200)
|
['g1_s1', 403],
|
||||||
response = json.loads(r.content)
|
['u1_s1', 403],
|
||||||
self.assertEqual(len(response), 1)
|
['a1_s1', 200],
|
||||||
storage_response = response[0]
|
])
|
||||||
self.assertEqual(storage_response['path'], self.sync.path)
|
def test_list_permission(arg, request):
|
||||||
|
c = request.getfixturevalue(arg[0])
|
||||||
|
assert c.get(reverse(LIST_URL)).status_code == arg[1]
|
||||||
|
|
||||||
def test_sync_update(self):
|
|
||||||
# can update sync as admin
|
def test_list_space(obj_1, obj_2, a1_s1, a1_s2, space_2):
|
||||||
r = self.admin_client_1.patch(
|
assert len(json.loads(a1_s1.get(reverse(LIST_URL)).content)) == 2
|
||||||
|
assert len(json.loads(a1_s2.get(reverse(LIST_URL)).content)) == 0
|
||||||
|
|
||||||
|
obj_1.space = space_2
|
||||||
|
obj_1.save()
|
||||||
|
|
||||||
|
assert len(json.loads(a1_s1.get(reverse(LIST_URL)).content)) == 1
|
||||||
|
assert len(json.loads(a1_s2.get(reverse(LIST_URL)).content)) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("arg", [
|
||||||
|
['a_u', 403],
|
||||||
|
['g1_s1', 403],
|
||||||
|
['u1_s1', 403],
|
||||||
|
['a1_s1', 200],
|
||||||
|
['g1_s2', 403],
|
||||||
|
['u1_s2', 403],
|
||||||
|
['a1_s2', 404],
|
||||||
|
])
|
||||||
|
def test_update(arg, request, obj_1):
|
||||||
|
c = request.getfixturevalue(arg[0])
|
||||||
|
r = c.patch(
|
||||||
reverse(
|
reverse(
|
||||||
'api:sync-detail',
|
DETAIL_URL,
|
||||||
args={self.sync.id}
|
args={obj_1.id}
|
||||||
),
|
),
|
||||||
{'path': 'new'},
|
{'path': 'new'},
|
||||||
content_type='application/json'
|
content_type='application/json'
|
||||||
)
|
)
|
||||||
response = json.loads(r.content)
|
response = json.loads(r.content)
|
||||||
self.assertEqual(r.status_code, 200)
|
assert r.status_code == arg[1]
|
||||||
self.assertEqual(response['path'], 'new')
|
if r.status_code == 200:
|
||||||
|
assert response['path'] == 'new'
|
||||||
|
|
||||||
def test_sync_delete(self):
|
|
||||||
# can delete sync as admin
|
@pytest.mark.parametrize("arg", [
|
||||||
r = self.admin_client_1.delete(
|
['a_u', 403],
|
||||||
reverse('api:sync-detail', args={self.sync.id})
|
['g1_s1', 403],
|
||||||
|
['u1_s1', 403],
|
||||||
|
['a1_s1', 201],
|
||||||
|
])
|
||||||
|
def test_add(arg, request, a1_s2, obj_1):
|
||||||
|
c = request.getfixturevalue(arg[0])
|
||||||
|
r = c.post(
|
||||||
|
reverse(LIST_URL),
|
||||||
|
{'storage': obj_1.storage.pk, 'path': 'test'},
|
||||||
|
content_type='application/json'
|
||||||
)
|
)
|
||||||
self.assertEqual(r.status_code, 204)
|
response = json.loads(r.content)
|
||||||
self.assertEqual(Sync.objects.count(), 0)
|
print(r.content)
|
||||||
|
assert r.status_code == arg[1]
|
||||||
|
if r.status_code == 201:
|
||||||
|
assert response['path'] == 'test'
|
||||||
|
r = c.get(reverse(DETAIL_URL, args={response['id']}))
|
||||||
|
assert r.status_code == 200
|
||||||
|
r = a1_s2.get(reverse(DETAIL_URL, args={response['id']}))
|
||||||
|
assert r.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete(a1_s1, a1_s2, obj_1):
|
||||||
|
r = a1_s2.delete(
|
||||||
|
reverse(
|
||||||
|
DETAIL_URL,
|
||||||
|
args={obj_1.id}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert r.status_code == 404
|
||||||
|
|
||||||
|
r = a1_s1.delete(
|
||||||
|
reverse(
|
||||||
|
DETAIL_URL,
|
||||||
|
args={obj_1.id}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert r.status_code == 204
|
||||||
|
with scopes_disabled():
|
||||||
|
assert Sync.objects.count() == 0
|
||||||
|
105
cookbook/tests/api/test_api_sync_log.py
Normal file
105
cookbook/tests/api/test_api_sync_log.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.contrib import auth
|
||||||
|
from django.urls import reverse
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
from cookbook.models import RecipeBook, Storage, Sync, SyncLog
|
||||||
|
|
||||||
|
LIST_URL = 'api:synclog-list'
|
||||||
|
DETAIL_URL = 'api:synclog-detail'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def obj_1(space_1, u1_s1):
|
||||||
|
s = Storage.objects.create(name='Test Storage 1', username='test', password='password', token='token', url='url', created_by=auth.get_user(u1_s1), space=space_1, )
|
||||||
|
sy = Sync.objects.create(storage=s, path='path', space=space_1, )
|
||||||
|
return SyncLog.objects.create(sync=sy, status=1)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def obj_2(space_1, u1_s1):
|
||||||
|
s = Storage.objects.create(name='Test Storage 2', username='test', password='password', token='token', url='url', created_by=auth.get_user(u1_s1), space=space_1, )
|
||||||
|
sy = Sync.objects.create(storage=s, path='path', space=space_1, )
|
||||||
|
return SyncLog.objects.create(sync=sy, status=1)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("arg", [
|
||||||
|
['a_u', 403],
|
||||||
|
['g1_s1', 403],
|
||||||
|
['u1_s1', 403],
|
||||||
|
['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, a1_s1, a1_s2, space_2):
|
||||||
|
assert len(json.loads(a1_s1.get(reverse(LIST_URL)).content)) == 2
|
||||||
|
assert len(json.loads(a1_s2.get(reverse(LIST_URL)).content)) == 0
|
||||||
|
|
||||||
|
obj_1.sync.space = space_2
|
||||||
|
obj_1.sync.save()
|
||||||
|
|
||||||
|
assert len(json.loads(a1_s1.get(reverse(LIST_URL)).content)) == 1
|
||||||
|
assert len(json.loads(a1_s2.get(reverse(LIST_URL)).content)) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("arg", [
|
||||||
|
['a_u', 403],
|
||||||
|
['g1_s1', 403],
|
||||||
|
['u1_s1', 403],
|
||||||
|
['a1_s1', 405],
|
||||||
|
['g1_s2', 403],
|
||||||
|
['u1_s2', 403],
|
||||||
|
['a1_s2', 405],
|
||||||
|
])
|
||||||
|
def test_update(arg, request, obj_1):
|
||||||
|
c = request.getfixturevalue(arg[0])
|
||||||
|
r = c.patch(
|
||||||
|
reverse(
|
||||||
|
DETAIL_URL,
|
||||||
|
args={obj_1.id}
|
||||||
|
),
|
||||||
|
{'msg': 'new'},
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
assert r.status_code == arg[1]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("arg", [
|
||||||
|
['a_u', 403],
|
||||||
|
['g1_s1', 403],
|
||||||
|
['u1_s1', 403],
|
||||||
|
['a1_s1', 405],
|
||||||
|
])
|
||||||
|
def test_add(arg, request, a1_s2, obj_1):
|
||||||
|
c = request.getfixturevalue(arg[0])
|
||||||
|
r = c.post(
|
||||||
|
reverse(LIST_URL),
|
||||||
|
{'msg': 'test'},
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
response = json.loads(r.content)
|
||||||
|
assert r.status_code == arg[1]
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete(a1_s1, a1_s2, obj_1):
|
||||||
|
r = a1_s2.delete(
|
||||||
|
reverse(
|
||||||
|
DETAIL_URL,
|
||||||
|
args={obj_1.id}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert r.status_code == 405
|
||||||
|
|
||||||
|
r = a1_s1.delete(
|
||||||
|
reverse(
|
||||||
|
DETAIL_URL,
|
||||||
|
args={obj_1.id}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert r.status_code == 405
|
@ -1,69 +1,148 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from cookbook.models import Unit
|
import pytest
|
||||||
from cookbook.tests.views.test_views import TestViews
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
from cookbook.models import Food, Unit
|
||||||
|
|
||||||
|
LIST_URL = 'api:unit-list'
|
||||||
|
DETAIL_URL = 'api:unit-detail'
|
||||||
|
|
||||||
|
|
||||||
class TestApiUnit(TestViews):
|
@pytest.fixture()
|
||||||
|
def obj_1(space_1):
|
||||||
|
return Unit.objects.get_or_create(name='test_1', space=space_1)[0]
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(TestApiUnit, self).setUp()
|
|
||||||
self.unit_1 = Unit.objects.create(
|
|
||||||
name='kg'
|
|
||||||
)
|
|
||||||
self.unit_2 = Unit.objects.create(
|
|
||||||
name='g'
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_keyword_list(self):
|
@pytest.fixture
|
||||||
# verify view permissions are applied accordingly
|
def obj_2(space_1):
|
||||||
self.batch_requests(
|
return Unit.objects.get_or_create(name='test_2', space=space_1)[0]
|
||||||
[
|
|
||||||
(self.anonymous_client, 403),
|
|
||||||
(self.guest_client_1, 403),
|
|
||||||
(self.user_client_1, 200),
|
|
||||||
(self.admin_client_1, 200),
|
|
||||||
(self.superuser_client, 200)
|
|
||||||
],
|
|
||||||
reverse('api:unit-list')
|
|
||||||
)
|
|
||||||
|
|
||||||
# verify storage is returned
|
|
||||||
r = self.user_client_1.get(reverse('api:unit-list'))
|
@pytest.mark.parametrize("arg", [
|
||||||
self.assertEqual(r.status_code, 200)
|
['a_u', 403],
|
||||||
|
['g1_s1', 403],
|
||||||
|
['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)) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_filter(obj_1, obj_2, u1_s1):
|
||||||
|
r = u1_s1.get(reverse(LIST_URL))
|
||||||
|
assert r.status_code == 200
|
||||||
response = json.loads(r.content)
|
response = json.loads(r.content)
|
||||||
self.assertEqual(len(response), 2)
|
assert len(response) == 2
|
||||||
self.assertEqual(response[0]['name'], self.unit_1.name)
|
assert response[0]['name'] == obj_1.name
|
||||||
|
|
||||||
r = self.user_client_1.get(f'{reverse("api:unit-list")}?limit=1')
|
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?limit=1').content)
|
||||||
response = json.loads(r.content)
|
assert len(response) == 1
|
||||||
self.assertEqual(len(response), 1)
|
|
||||||
|
|
||||||
r = self.user_client_1.get(f'{reverse("api:unit-list")}?query=m')
|
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query=chicken').content)
|
||||||
response = json.loads(r.content)
|
assert len(response) == 0
|
||||||
self.assertEqual(len(response), 0)
|
|
||||||
|
|
||||||
r = self.user_client_1.get(f'{reverse("api:unit-list")}?query=kg')
|
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query={obj_1.name[4:]}').content)
|
||||||
response = json.loads(r.content)
|
assert len(response) == 1
|
||||||
self.assertEqual(len(response), 1)
|
|
||||||
|
|
||||||
def test_keyword_update(self):
|
|
||||||
r = self.user_client_1.patch(
|
@pytest.mark.parametrize("arg", [
|
||||||
|
['a_u', 403],
|
||||||
|
['g1_s1', 403],
|
||||||
|
['u1_s1', 200],
|
||||||
|
['a1_s1', 200],
|
||||||
|
['g1_s2', 403],
|
||||||
|
['u1_s2', 404],
|
||||||
|
['a1_s2', 404],
|
||||||
|
])
|
||||||
|
def test_update(arg, request, obj_1):
|
||||||
|
c = request.getfixturevalue(arg[0])
|
||||||
|
r = c.patch(
|
||||||
reverse(
|
reverse(
|
||||||
'api:unit-detail',
|
DETAIL_URL,
|
||||||
args={self.unit_1.id}
|
args={obj_1.id}
|
||||||
),
|
),
|
||||||
{'name': 'new'},
|
{'name': 'new'},
|
||||||
content_type='application/json'
|
content_type='application/json'
|
||||||
)
|
)
|
||||||
response = json.loads(r.content)
|
response = json.loads(r.content)
|
||||||
self.assertEqual(r.status_code, 200)
|
assert r.status_code == arg[1]
|
||||||
self.assertEqual(response['name'], 'new')
|
if r.status_code == 200:
|
||||||
|
assert response['name'] == 'new'
|
||||||
|
|
||||||
def test_keyword_delete(self):
|
|
||||||
r = self.user_client_1.delete(
|
@pytest.mark.parametrize("arg", [
|
||||||
reverse('api:unit-detail', args={self.unit_1.id})
|
['a_u', 403],
|
||||||
|
['g1_s1', 403],
|
||||||
|
['u1_s1', 201],
|
||||||
|
['a1_s1', 201],
|
||||||
|
])
|
||||||
|
def test_add(arg, request, u1_s2):
|
||||||
|
c = request.getfixturevalue(arg[0])
|
||||||
|
r = c.post(
|
||||||
|
reverse(LIST_URL),
|
||||||
|
{'name': 'test'},
|
||||||
|
content_type='application/json'
|
||||||
)
|
)
|
||||||
self.assertEqual(r.status_code, 204)
|
response = json.loads(r.content)
|
||||||
self.assertEqual(Unit.objects.count(), 1)
|
assert r.status_code == arg[1]
|
||||||
|
if r.status_code == 201:
|
||||||
|
assert response['name'] == 'test'
|
||||||
|
r = c.get(reverse(DETAIL_URL, args={response['id']}))
|
||||||
|
assert r.status_code == 200
|
||||||
|
r = u1_s2.get(reverse(DETAIL_URL, args={response['id']}))
|
||||||
|
assert r.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_duplicate(u1_s1, u1_s2, obj_1):
|
||||||
|
r = u1_s1.post(
|
||||||
|
reverse(LIST_URL),
|
||||||
|
{'name': obj_1.name},
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
response = json.loads(r.content)
|
||||||
|
assert r.status_code == 201
|
||||||
|
assert response['id'] == obj_1.id
|
||||||
|
|
||||||
|
r = u1_s2.post(
|
||||||
|
reverse(LIST_URL),
|
||||||
|
{'name': obj_1.name},
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
response = json.loads(r.content)
|
||||||
|
assert r.status_code == 201
|
||||||
|
assert response['id'] != obj_1.id
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
with scopes_disabled():
|
||||||
|
assert Food.objects.count() == 0
|
||||||
|
@ -1,41 +1,69 @@
|
|||||||
from cookbook.tests.views.test_views import TestViews
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from django.contrib import auth
|
from django.contrib import auth
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
LIST_URL = 'api:username-list'
|
||||||
|
DETAIL_URL = 'api:username-detail'
|
||||||
|
|
||||||
class TestApiUsername(TestViews):
|
|
||||||
|
|
||||||
def setUp(self):
|
def test_forbidden_methods(u1_s1):
|
||||||
super(TestApiUsername, self).setUp()
|
r = u1_s1.post(
|
||||||
|
reverse(LIST_URL))
|
||||||
|
assert r.status_code == 405
|
||||||
|
|
||||||
def test_forbidden_methods(self):
|
r = u1_s1.put(
|
||||||
r = self.user_client_1.post(
|
|
||||||
reverse('api:username-list'))
|
|
||||||
self.assertEqual(r.status_code, 405)
|
|
||||||
|
|
||||||
r = self.user_client_1.put(
|
|
||||||
reverse(
|
reverse(
|
||||||
'api:username-detail',
|
DETAIL_URL,
|
||||||
args=[auth.get_user(self.user_client_1).pk])
|
args=[auth.get_user(u1_s1).pk])
|
||||||
)
|
)
|
||||||
self.assertEqual(r.status_code, 405)
|
assert r.status_code == 405
|
||||||
|
|
||||||
r = self.user_client_1.delete(
|
r = u1_s1.delete(
|
||||||
reverse(
|
reverse(
|
||||||
'api:username-detail',
|
DETAIL_URL,
|
||||||
args=[auth.get_user(self.user_client_1).pk]
|
args=[auth.get_user(u1_s1).pk]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.assertEqual(r.status_code, 405)
|
assert r.status_code == 405
|
||||||
|
|
||||||
def test_username_list(self):
|
|
||||||
self.batch_requests(
|
@pytest.mark.parametrize("arg", [
|
||||||
[
|
['a_u', 403],
|
||||||
(self.anonymous_client, 403),
|
['g1_s1', 200],
|
||||||
(self.guest_client_1, 200),
|
['u1_s1', 200],
|
||||||
(self.user_client_1, 200),
|
['a1_s1', 200],
|
||||||
(self.admin_client_1, 200),
|
])
|
||||||
(self.superuser_client, 200)
|
def test_list(arg, request):
|
||||||
],
|
c = request.getfixturevalue(arg[0])
|
||||||
reverse('api:username-list')
|
assert c.get(reverse(LIST_URL)).status_code == arg[1]
|
||||||
)
|
|
||||||
|
|
||||||
|
def test_list_filter(u1_s1, u2_s1, u1_s2, u2_s2):
|
||||||
|
r = u1_s1.get(reverse(LIST_URL))
|
||||||
|
assert r.status_code == 200
|
||||||
|
response = json.loads(r.content)
|
||||||
|
assert len(response) == 2
|
||||||
|
|
||||||
|
obj_u2_s1 = auth.get_user(u2_s1)
|
||||||
|
obj_u2_s2 = auth.get_user(u2_s2)
|
||||||
|
|
||||||
|
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?filter_list=[{obj_u2_s1.pk},{obj_u2_s2.pk}]').content)
|
||||||
|
assert len(response) == 1
|
||||||
|
|
||||||
|
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?filter_list=[]').content)
|
||||||
|
assert len(response) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_space(u1_s1, u2_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)) == 1
|
||||||
|
|
||||||
|
u = auth.get_user(u2_s1)
|
||||||
|
u.userpreference.space = space_2
|
||||||
|
u.userpreference.save()
|
||||||
|
|
||||||
|
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 1
|
||||||
|
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 2
|
||||||
|
@ -1,149 +1,111 @@
|
|||||||
|
from cookbook.models import UserPreference
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from cookbook.models import UserPreference
|
import pytest
|
||||||
from cookbook.tests.views.test_views import TestViews
|
|
||||||
from django.contrib import auth
|
from django.contrib import auth
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
LIST_URL = 'api:userpreference-list'
|
||||||
|
DETAIL_URL = 'api:userpreference-detail'
|
||||||
|
|
||||||
|
|
||||||
class TestApiUserPreference(TestViews):
|
def test_add(u1_s1, u2_s1):
|
||||||
|
r = u1_s1.post(reverse(LIST_URL))
|
||||||
|
assert r.status_code == 400
|
||||||
|
|
||||||
def setUp(self):
|
with scopes_disabled():
|
||||||
super(TestApiUserPreference, self).setUp()
|
UserPreference.objects.filter(user=auth.get_user(u1_s1)).delete()
|
||||||
|
|
||||||
def test_preference_create(self):
|
r = u2_s1.post(reverse(LIST_URL), {'user': auth.get_user(u1_s1).id}, content_type='application/json')
|
||||||
r = self.user_client_1.post(reverse('api:userpreference-list'))
|
assert r.status_code == 404
|
||||||
self.assertEqual(r.status_code, 201)
|
|
||||||
response = json.loads(r.content)
|
|
||||||
self.assertEqual(
|
|
||||||
response['user'], auth.get_user(self.user_client_1).id
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
response['theme'],
|
|
||||||
UserPreference._meta.get_field('theme').get_default()
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_preference_list(self):
|
r = u1_s1.post(reverse(LIST_URL), {'user': auth.get_user(u1_s1).id}, content_type='application/json')
|
||||||
UserPreference.objects.create(user=auth.get_user(self.user_client_1))
|
assert r.status_code == 200
|
||||||
UserPreference.objects.create(user=auth.get_user(self.guest_client_1))
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_preference_list(u1_s1, u2_s1, u1_s2):
|
||||||
# users can only see own preference in list
|
# users can only see own preference in list
|
||||||
r = self.user_client_1.get(reverse('api:userpreference-list'))
|
r = u1_s1.get(reverse(LIST_URL))
|
||||||
self.assertEqual(r.status_code, 200)
|
assert r.status_code == 200
|
||||||
response = json.loads(r.content)
|
response = json.loads(r.content)
|
||||||
self.assertEqual(len(response), 1)
|
assert len(response) == 1
|
||||||
self.assertEqual(
|
assert response[0]['user'] == auth.get_user(u1_s1).id
|
||||||
response[0]['user'], auth.get_user(self.user_client_1).id
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("arg", [
|
||||||
|
['a_u', 403],
|
||||||
|
['g1_s1', 404],
|
||||||
|
['u1_s1', 200],
|
||||||
|
['a1_s1', 404],
|
||||||
|
])
|
||||||
|
def test_preference_retrieve(arg, request, u1_s1):
|
||||||
|
c = request.getfixturevalue(arg[0])
|
||||||
|
|
||||||
|
r = c.get(
|
||||||
|
reverse(DETAIL_URL, args={auth.get_user(u1_s1).id}),
|
||||||
)
|
)
|
||||||
|
assert r.status_code == arg[1]
|
||||||
|
|
||||||
# superusers can see all user prefs in list
|
|
||||||
r = self.superuser_client.get(reverse('api:userpreference-list'))
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
response = json.loads(r.content)
|
|
||||||
self.assertEqual(len(response), 2)
|
|
||||||
|
|
||||||
def test_preference_retrieve(self):
|
|
||||||
UserPreference.objects.create(user=auth.get_user(self.user_client_1))
|
|
||||||
UserPreference.objects.create(user=auth.get_user(self.guest_client_1))
|
|
||||||
|
|
||||||
self.batch_requests(
|
|
||||||
[
|
|
||||||
(self.guest_client_1, 404),
|
|
||||||
(self.user_client_1, 200),
|
|
||||||
(self.user_client_2, 404),
|
|
||||||
(self.anonymous_client, 403),
|
|
||||||
(self.admin_client_1, 404),
|
|
||||||
(self.superuser_client, 200)
|
|
||||||
],
|
|
||||||
reverse(
|
|
||||||
'api:userpreference-detail',
|
|
||||||
args={auth.get_user(self.user_client_1).id}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_preference_update(self):
|
|
||||||
UserPreference.objects.create(user=auth.get_user(self.user_client_1))
|
|
||||||
UserPreference.objects.create(user=auth.get_user(self.guest_client_1))
|
|
||||||
|
|
||||||
|
def test_preference_update(u1_s1, u2_s1):
|
||||||
# can update users preference
|
# can update users preference
|
||||||
r = self.user_client_1.put(
|
r = u1_s1.put(
|
||||||
reverse(
|
reverse(
|
||||||
'api:userpreference-detail',
|
DETAIL_URL,
|
||||||
args={auth.get_user(self.user_client_1).id}
|
args={auth.get_user(u1_s1).id}
|
||||||
),
|
),
|
||||||
{'theme': UserPreference.DARKLY},
|
{'user': auth.get_user(u1_s1).id, 'theme': UserPreference.DARKLY},
|
||||||
content_type='application/json'
|
content_type='application/json'
|
||||||
)
|
)
|
||||||
response = json.loads(r.content)
|
response = json.loads(r.content)
|
||||||
self.assertEqual(r.status_code, 200)
|
assert r.status_code == 200
|
||||||
self.assertEqual(response['theme'], UserPreference.DARKLY)
|
assert response['theme'] == UserPreference.DARKLY
|
||||||
|
|
||||||
# cant set another users non existent pref
|
# cant set another users non existent pref
|
||||||
r = self.user_client_1.put(
|
r = u1_s1.put(
|
||||||
reverse(
|
reverse(
|
||||||
'api:userpreference-detail',
|
DETAIL_URL,
|
||||||
args={auth.get_user(self.user_client_2).id}
|
args={auth.get_user(u2_s1).id}
|
||||||
),
|
),
|
||||||
{'theme': UserPreference.DARKLY},
|
{'user': auth.get_user(u1_s1).id, 'theme': UserPreference.DARKLY},
|
||||||
content_type='application/json'
|
content_type='application/json'
|
||||||
)
|
)
|
||||||
self.assertEqual(r.status_code, 404)
|
assert r.status_code == 404
|
||||||
|
|
||||||
# cant set another users existent pref
|
# cant set another users existent pref
|
||||||
r = self.user_client_2.put(
|
with scopes_disabled():
|
||||||
|
UserPreference.objects.filter(user=auth.get_user(u2_s1)).delete()
|
||||||
|
|
||||||
|
r = u1_s1.put(
|
||||||
reverse(
|
reverse(
|
||||||
'api:userpreference-detail',
|
DETAIL_URL,
|
||||||
args={auth.get_user(self.user_client_1).id}
|
args={auth.get_user(u2_s1).id}
|
||||||
),
|
),
|
||||||
{'theme': UserPreference.FLATLY},
|
{'user': auth.get_user(u1_s1).id, 'theme': UserPreference.FLATLY},
|
||||||
content_type='application/json'
|
content_type='application/json'
|
||||||
)
|
)
|
||||||
self.assertEqual(r.status_code, 404)
|
assert r.status_code == 404
|
||||||
|
with scopes_disabled():
|
||||||
|
assert not UserPreference.objects.filter(user=auth.get_user(u2_s1)).exists()
|
||||||
|
|
||||||
# can set pref as superuser
|
|
||||||
r = self.superuser_client.put(
|
def test_preference_delete(u1_s1, u2_s1):
|
||||||
|
# cant delete other preference
|
||||||
|
r = u1_s1.delete(
|
||||||
reverse(
|
reverse(
|
||||||
'api:userpreference-detail',
|
DETAIL_URL,
|
||||||
args={auth.get_user(self.user_client_1).id}
|
args={auth.get_user(u2_s1).id}
|
||||||
),
|
|
||||||
{'theme': UserPreference.FLATLY},
|
|
||||||
content_type='application/json'
|
|
||||||
)
|
)
|
||||||
self.assertEqual(r.status_code, 200)
|
)
|
||||||
|
assert r.status_code == 404
|
||||||
def test_preference_delete(self):
|
|
||||||
UserPreference.objects.create(user=auth.get_user(self.user_client_1))
|
|
||||||
|
|
||||||
# can delete own preference
|
# can delete own preference
|
||||||
r = self.user_client_1.delete(
|
r = u1_s1.delete(
|
||||||
reverse(
|
reverse(
|
||||||
'api:userpreference-detail',
|
DETAIL_URL,
|
||||||
args={auth.get_user(self.user_client_1).id}
|
args={auth.get_user(u1_s1).id}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.assertEqual(r.status_code, 204)
|
assert r.status_code == 204
|
||||||
self.assertEqual(UserPreference.objects.count(), 0)
|
|
||||||
|
|
||||||
UserPreference.objects.create(user=auth.get_user(self.user_client_1
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# cant delete other preference
|
|
||||||
r = self.user_client_2.delete(
|
|
||||||
reverse(
|
|
||||||
'api:userpreference-detail',
|
|
||||||
args={auth.get_user(self.user_client_1).id}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.assertEqual(r.status_code, 404)
|
|
||||||
self.assertEqual(UserPreference.objects.count(), 1)
|
|
||||||
|
|
||||||
# superuser can delete everything
|
|
||||||
r = self.superuser_client.delete(
|
|
||||||
reverse(
|
|
||||||
'api:userpreference-detail',
|
|
||||||
args={auth.get_user(self.user_client_1).id}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.assertEqual(r.status_code, 204)
|
|
||||||
self.assertEqual(UserPreference.objects.count(), 0)
|
|
||||||
|
113
cookbook/tests/api/test_api_view_log.py
Normal file
113
cookbook/tests/api/test_api_view_log.py
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.contrib import auth
|
||||||
|
from django.urls import reverse
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
from cookbook.models import Keyword, CookLog, ViewLog
|
||||||
|
|
||||||
|
LIST_URL = 'api:viewlog-list'
|
||||||
|
DETAIL_URL = 'api:viewlog-detail'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def obj_1(space_1, u1_s1, recipe_1_s1):
|
||||||
|
return ViewLog.objects.create(recipe=recipe_1_s1, created_by=auth.get_user(u1_s1), space=space_1)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def obj_2(space_1, u1_s1, recipe_1_s1):
|
||||||
|
return ViewLog.objects.create(recipe=recipe_1_s1, 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
|
||||||
|
|
||||||
|
|
||||||
|
@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'
|
||||||
|
)
|
||||||
|
assert r.status_code == arg[1]
|
||||||
|
|
||||||
|
|
||||||
|
# TODO disabled until https://github.com/vabene1111/recipes/issues/484
|
||||||
|
|
||||||
|
# @pytest.mark.parametrize("arg", [
|
||||||
|
# ['a_u', 403],
|
||||||
|
# ['g1_s1', 201],
|
||||||
|
# ['u1_s1', 201],
|
||||||
|
# ['a1_s1', 201],
|
||||||
|
# ])
|
||||||
|
# def test_add(arg, request, u1_s2, u2_s1, recipe_1_s1):
|
||||||
|
# c = request.getfixturevalue(arg[0])
|
||||||
|
# r = c.post(
|
||||||
|
# reverse(LIST_URL),
|
||||||
|
# {'recipe': recipe_1_s1.id},
|
||||||
|
# content_type='application/json'
|
||||||
|
# )
|
||||||
|
# response = json.loads(r.content)
|
||||||
|
# assert r.status_code == arg[1]
|
||||||
|
# if r.status_code == 201:
|
||||||
|
# assert response['recipe'] == recipe_1_s1.id
|
||||||
|
# r = c.get(reverse(DETAIL_URL, args={response['id']}))
|
||||||
|
# assert r.status_code == 200
|
||||||
|
# r = u2_s1.get(reverse(DETAIL_URL, args={response['id']}))
|
||||||
|
# assert r.status_code == 404
|
||||||
|
# r = u1_s2.get(reverse(DETAIL_URL, args={response['id']}))
|
||||||
|
# assert r.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
with scopes_disabled():
|
||||||
|
assert ViewLog.objects.count() == 0
|
197
cookbook/tests/conftest.py
Normal file
197
cookbook/tests/conftest.py
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
import copy
|
||||||
|
import inspect
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.contrib import auth
|
||||||
|
from django.contrib.auth.models import User, Group
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
from cookbook.models import Space, Recipe, Step, Ingredient, Food, Unit, Storage
|
||||||
|
|
||||||
|
|
||||||
|
# hack from https://github.com/raphaelm/django-scopes to disable scopes for all fixtures
|
||||||
|
# does not work on yield fixtures as only one yield can be used per fixture (i think)
|
||||||
|
@pytest.hookimpl(hookwrapper=True)
|
||||||
|
def pytest_fixture_setup(fixturedef, request):
|
||||||
|
if inspect.isgeneratorfunction(fixturedef.func):
|
||||||
|
yield
|
||||||
|
else:
|
||||||
|
with scopes_disabled():
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def enable_db_access_for_all_tests(db):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def space_1():
|
||||||
|
with scopes_disabled():
|
||||||
|
return Space.objects.get_or_create(name='space_1')[0]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def space_2():
|
||||||
|
with scopes_disabled():
|
||||||
|
return Space.objects.get_or_create(name='space_2')[0]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------- OBJECT FIXTURES ---------------------
|
||||||
|
|
||||||
|
def get_random_recipe(space_1, u1_s1):
|
||||||
|
r = Recipe.objects.create(
|
||||||
|
name=uuid.uuid4(),
|
||||||
|
waiting_time=20,
|
||||||
|
working_time=20,
|
||||||
|
servings=4,
|
||||||
|
created_by=auth.get_user(u1_s1),
|
||||||
|
space=space_1,
|
||||||
|
internal=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
s1 = Step.objects.create(name=uuid.uuid4(), instruction=uuid.uuid4(), )
|
||||||
|
s2 = Step.objects.create(name=uuid.uuid4(), instruction=uuid.uuid4(), )
|
||||||
|
|
||||||
|
r.steps.add(s1)
|
||||||
|
r.steps.add(s2)
|
||||||
|
|
||||||
|
for x in range(5):
|
||||||
|
s1.ingredients.add(
|
||||||
|
Ingredient.objects.create(
|
||||||
|
amount=1,
|
||||||
|
food=Food.objects.create(name=uuid.uuid4(), space=space_1, ),
|
||||||
|
unit=Unit.objects.create(name=uuid.uuid4(), space=space_1, ),
|
||||||
|
note=uuid.uuid4(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
s2.ingredients.add(
|
||||||
|
Ingredient.objects.create(
|
||||||
|
amount=1,
|
||||||
|
food=Food.objects.create(name=uuid.uuid4(), space=space_1, ),
|
||||||
|
unit=Unit.objects.create(name=uuid.uuid4(), space=space_1, ),
|
||||||
|
note=uuid.uuid4(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def recipe_1_s1(space_1, u1_s1):
|
||||||
|
return get_random_recipe(space_1, u1_s1)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def recipe_2_s1(space_1, u1_s1):
|
||||||
|
return get_random_recipe(space_1, u1_s1)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def ext_recipe_1_s1(space_1, u1_s1):
|
||||||
|
r = get_random_recipe(space_1, u1_s1)
|
||||||
|
r.internal = False
|
||||||
|
r.link = 'test'
|
||||||
|
r.save()
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------- USER FIXTURES -----------------------
|
||||||
|
# maybe better with factories but this is very explict so ...
|
||||||
|
|
||||||
|
def create_user(client, space, **kwargs):
|
||||||
|
c = copy.deepcopy(client)
|
||||||
|
with scopes_disabled():
|
||||||
|
group = kwargs.pop('group', None)
|
||||||
|
username = kwargs.pop('username', uuid.uuid4())
|
||||||
|
|
||||||
|
user = User.objects.create(username=username, **kwargs)
|
||||||
|
if group:
|
||||||
|
user.groups.add(Group.objects.get(name=group))
|
||||||
|
|
||||||
|
user.userpreference.space = space
|
||||||
|
user.userpreference.save()
|
||||||
|
c.force_login(user)
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
# anonymous user
|
||||||
|
@pytest.fixture()
|
||||||
|
def a_u(client):
|
||||||
|
return copy.deepcopy(client)
|
||||||
|
|
||||||
|
|
||||||
|
# users without any group
|
||||||
|
@pytest.fixture()
|
||||||
|
def ng1_s1(client, space_1):
|
||||||
|
return create_user(client, space_1)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def ng1_s2(client, space_2):
|
||||||
|
return create_user(client, space_2)
|
||||||
|
|
||||||
|
|
||||||
|
# guests
|
||||||
|
@pytest.fixture()
|
||||||
|
def g1_s1(client, space_1):
|
||||||
|
return create_user(client, space_1, group='guest')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def g2_s1(client, space_1):
|
||||||
|
return create_user(client, space_1, group='guest')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def g1_s2(client, space_2):
|
||||||
|
return create_user(client, space_2, group='guest')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def g2_s2(client, space_2):
|
||||||
|
return create_user(client, space_2, group='guest')
|
||||||
|
|
||||||
|
|
||||||
|
# users
|
||||||
|
@pytest.fixture()
|
||||||
|
def u1_s1(client, space_1):
|
||||||
|
return create_user(client, space_1, group='user')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def u2_s1(client, space_1):
|
||||||
|
return create_user(client, space_1, group='user')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def u1_s2(client, space_2):
|
||||||
|
return create_user(client, space_2, group='user')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def u2_s2(client, space_2):
|
||||||
|
return create_user(client, space_2, group='user')
|
||||||
|
|
||||||
|
|
||||||
|
# admins
|
||||||
|
@pytest.fixture()
|
||||||
|
def a1_s1(client, space_1):
|
||||||
|
return create_user(client, space_1, group='admin')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def a2_s1(client, space_1):
|
||||||
|
return create_user(client, space_1, group='admin')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def a1_s2(client, space_2):
|
||||||
|
return create_user(client, space_2, group='admin')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def a2_s2(client, space_2):
|
||||||
|
return create_user(client, space_2, group='admin')
|
@ -0,0 +1,7 @@
|
|||||||
|
from django.test import utils
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
# disables scoping error in all queries used inside the test FUNCTIONS
|
||||||
|
# FIXTURES need to have their own scopes_disabled!!
|
||||||
|
# This is done by hook pytest_fixture_setup in conftest.py for all non yield fixtures
|
||||||
|
utils.setup_databases = scopes_disabled()(utils.setup_databases)
|
@ -1,57 +0,0 @@
|
|||||||
from cookbook.models import Comment, Recipe
|
|
||||||
from cookbook.tests.views.test_views import TestViews
|
|
||||||
from django.contrib import auth
|
|
||||||
from django.urls import reverse
|
|
||||||
|
|
||||||
|
|
||||||
class TestEditsComment(TestViews):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(TestEditsComment, self).setUp()
|
|
||||||
|
|
||||||
self.recipe = Recipe.objects.create(
|
|
||||||
internal=True,
|
|
||||||
working_time=1,
|
|
||||||
waiting_time=1,
|
|
||||||
created_by=auth.get_user(self.user_client_1)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.comment = Comment.objects.create(
|
|
||||||
text='TestStorage',
|
|
||||||
created_by=auth.get_user(self.guest_client_1),
|
|
||||||
recipe=self.recipe
|
|
||||||
)
|
|
||||||
self.url = reverse('edit_comment', args=[self.comment.pk])
|
|
||||||
|
|
||||||
def test_new_comment(self):
|
|
||||||
r = self.user_client_1.post(
|
|
||||||
reverse(
|
|
||||||
'view_recipe',
|
|
||||||
args=[self.recipe.pk]
|
|
||||||
),
|
|
||||||
{
|
|
||||||
'comment-text': 'Test Comment Text',
|
|
||||||
'comment-recipe': self.recipe.pk
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
|
|
||||||
def test_edit_comment_permissions(self):
|
|
||||||
r = self.anonymous_client.get(self.url)
|
|
||||||
self.assertEqual(r.status_code, 302)
|
|
||||||
|
|
||||||
r = self.guest_client_1.get(self.url)
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
|
|
||||||
r = self.guest_client_2.get(self.url)
|
|
||||||
self.assertEqual(r.status_code, 302)
|
|
||||||
|
|
||||||
r = self.user_client_1.get(self.url)
|
|
||||||
self.assertEqual(r.status_code, 302)
|
|
||||||
|
|
||||||
r = self.admin_client_1.get(self.url)
|
|
||||||
self.assertEqual(r.status_code, 302)
|
|
||||||
|
|
||||||
r = self.superuser_client.get(self.url)
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
@ -1,156 +1,82 @@
|
|||||||
from cookbook.models import Food, Recipe, Storage, Unit
|
from cookbook.models import Recipe, Storage
|
||||||
from cookbook.tests.views.test_views import TestViews
|
|
||||||
from django.contrib import auth
|
from django.contrib import auth
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from pytest_django.asserts import assertTemplateUsed
|
||||||
|
|
||||||
|
|
||||||
class TestEditsRecipe(TestViews):
|
def test_switch_recipe(u1_s1, recipe_1_s1, space_1):
|
||||||
|
|
||||||
def test_switch_recipe(self):
|
|
||||||
internal_recipe = Recipe.objects.create(
|
|
||||||
name='Test',
|
|
||||||
internal=True,
|
|
||||||
created_by=auth.get_user(self.user_client_1)
|
|
||||||
)
|
|
||||||
|
|
||||||
external_recipe = Recipe.objects.create(
|
external_recipe = Recipe.objects.create(
|
||||||
name='Test',
|
name='Test',
|
||||||
internal=False,
|
internal=False,
|
||||||
created_by=auth.get_user(self.user_client_1)
|
created_by=auth.get_user(u1_s1),
|
||||||
|
space=space_1,
|
||||||
)
|
)
|
||||||
|
|
||||||
url = reverse('edit_recipe', args=[internal_recipe.pk])
|
url = reverse('edit_recipe', args=[recipe_1_s1.pk])
|
||||||
r = self.user_client_1.get(url)
|
r = u1_s1.get(url)
|
||||||
self.assertEqual(r.status_code, 302)
|
assert r.status_code == 302
|
||||||
|
|
||||||
r = self.user_client_1.get(r.url)
|
r = u1_s1.get(r.url)
|
||||||
self.assertTemplateUsed(r, 'forms/edit_internal_recipe.html')
|
assertTemplateUsed(r, 'forms/edit_internal_recipe.html')
|
||||||
|
|
||||||
url = reverse('edit_recipe', args=[external_recipe.pk])
|
url = reverse('edit_recipe', args=[external_recipe.pk])
|
||||||
r = self.user_client_1.get(url)
|
r = u1_s1.get(url)
|
||||||
self.assertEqual(r.status_code, 302)
|
assert r.status_code == 302
|
||||||
|
|
||||||
r = self.user_client_1.get(r.url)
|
r = u1_s1.get(r.url)
|
||||||
self.assertTemplateUsed(r, 'generic/edit_template.html')
|
assertTemplateUsed(r, 'generic/edit_template.html')
|
||||||
|
|
||||||
def test_convert_recipe(self):
|
|
||||||
url = reverse('edit_convert_recipe', args=[42])
|
|
||||||
r = self.user_client_1.get(url)
|
|
||||||
self.assertEqual(r.status_code, 404)
|
|
||||||
|
|
||||||
|
def test_convert_recipe(u1_s1, space_1):
|
||||||
external_recipe = Recipe.objects.create(
|
external_recipe = Recipe.objects.create(
|
||||||
name='Test',
|
name='Test',
|
||||||
internal=False,
|
internal=False,
|
||||||
created_by=auth.get_user(self.user_client_1)
|
created_by=auth.get_user(u1_s1),
|
||||||
|
space=space_1,
|
||||||
)
|
)
|
||||||
|
|
||||||
url = reverse('edit_convert_recipe', args=[external_recipe.pk])
|
r = u1_s1.get(reverse('edit_convert_recipe', args=[external_recipe.pk]))
|
||||||
r = self.user_client_1.get(url)
|
assert r.status_code == 302
|
||||||
self.assertEqual(r.status_code, 302)
|
|
||||||
|
|
||||||
recipe = Recipe.objects.get(pk=external_recipe.pk)
|
external_recipe.refresh_from_db()
|
||||||
self.assertTrue(recipe.internal)
|
assert external_recipe.internal
|
||||||
|
|
||||||
url = reverse('edit_convert_recipe', args=[recipe.pk])
|
|
||||||
r = self.user_client_1.get(url)
|
|
||||||
self.assertEqual(r.status_code, 302)
|
|
||||||
|
|
||||||
def test_internal_recipe_update(self):
|
def test_external_recipe_update(u1_s1, u1_s2, space_1):
|
||||||
recipe = Recipe.objects.create(
|
|
||||||
name='Test',
|
|
||||||
created_by=auth.get_user(self.user_client_1)
|
|
||||||
)
|
|
||||||
|
|
||||||
url = reverse('api:recipe-detail', args=[recipe.pk])
|
|
||||||
|
|
||||||
r = self.user_client_1.get(url)
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
|
|
||||||
r = self.anonymous_client.get(url)
|
|
||||||
self.assertEqual(r.status_code, 403)
|
|
||||||
|
|
||||||
r = self.user_client_1.put(
|
|
||||||
url,
|
|
||||||
{
|
|
||||||
'name': 'Changed',
|
|
||||||
'working_time': 15,
|
|
||||||
'waiting_time': 15,
|
|
||||||
'keywords': [],
|
|
||||||
'steps': []
|
|
||||||
},
|
|
||||||
content_type='application/json'
|
|
||||||
)
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
|
|
||||||
recipe = Recipe.objects.get(pk=recipe.pk)
|
|
||||||
self.assertEqual('Changed', recipe.name)
|
|
||||||
|
|
||||||
Food.objects.create(name='Egg')
|
|
||||||
Unit.objects.create(name='g')
|
|
||||||
|
|
||||||
r = self.user_client_1.put(
|
|
||||||
url,
|
|
||||||
{
|
|
||||||
'name': 'Changed',
|
|
||||||
'working_time': 15,
|
|
||||||
'waiting_time': 15,
|
|
||||||
'keywords': [],
|
|
||||||
'steps': [
|
|
||||||
{
|
|
||||||
'ingredients': [
|
|
||||||
{
|
|
||||||
'food': {'name': 'test food'},
|
|
||||||
'unit': {'name': 'test unit'},
|
|
||||||
'amount': 12, 'note': 'test note'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'food': {'name': 'test food 2'},
|
|
||||||
'unit': {'name': 'test unit 2'},
|
|
||||||
'amount': 42, 'note': 'test note 2'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
content_type='application/json'
|
|
||||||
)
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
self.assertEqual(2, recipe.steps.first().ingredients.count())
|
|
||||||
|
|
||||||
with open('cookbook/tests/resources/image.jpg', 'rb') as file: # noqa: E501,F841
|
|
||||||
pass # TODO new image tests
|
|
||||||
|
|
||||||
with open('cookbook/tests/resources/image.png', 'rb') as file: # noqa: E501,F841
|
|
||||||
pass # TODO new image tests
|
|
||||||
|
|
||||||
def test_external_recipe_update(self):
|
|
||||||
storage = Storage.objects.create(
|
storage = Storage.objects.create(
|
||||||
name='TestStorage',
|
name='TestStorage',
|
||||||
method=Storage.DROPBOX,
|
method=Storage.DROPBOX,
|
||||||
created_by=auth.get_user(self.user_client_1),
|
created_by=auth.get_user(u1_s1),
|
||||||
token='test',
|
token='test',
|
||||||
username='test',
|
username='test',
|
||||||
password='test',
|
password='test',
|
||||||
|
space=space_1,
|
||||||
)
|
)
|
||||||
|
|
||||||
recipe = Recipe.objects.create(
|
recipe = Recipe.objects.create(
|
||||||
name='Test',
|
name='Test',
|
||||||
created_by=auth.get_user(self.user_client_1),
|
created_by=auth.get_user(u1_s1),
|
||||||
storage=storage,
|
storage=storage,
|
||||||
|
space=space_1,
|
||||||
)
|
)
|
||||||
|
|
||||||
url = reverse('edit_external_recipe', args=[recipe.pk])
|
url = reverse('edit_external_recipe', args=[recipe.pk])
|
||||||
|
|
||||||
r = self.user_client_1.get(url)
|
r = u1_s1.get(url)
|
||||||
self.assertEqual(r.status_code, 200)
|
assert r.status_code == 200
|
||||||
|
|
||||||
r = self.anonymous_client.get(url)
|
u1_s2.post(
|
||||||
self.assertEqual(r.status_code, 302)
|
|
||||||
|
|
||||||
r = self.user_client_1.post(
|
|
||||||
url,
|
url,
|
||||||
{'name': 'Test', 'working_time': 15, 'waiting_time': 15, 'servings': 1, }
|
{'name': 'Test', 'working_time': 15, 'waiting_time': 15, 'servings': 1, }
|
||||||
)
|
)
|
||||||
recipe.refresh_from_db()
|
recipe.refresh_from_db()
|
||||||
self.assertEqual(recipe.working_time, 15)
|
assert recipe.working_time == 0
|
||||||
self.assertEqual(recipe.waiting_time, 15)
|
assert recipe.waiting_time == 0
|
||||||
|
|
||||||
|
u1_s1.post(
|
||||||
|
url,
|
||||||
|
{'name': 'Test', 'working_time': 15, 'waiting_time': 15, 'servings': 1, }
|
||||||
|
)
|
||||||
|
recipe.refresh_from_db()
|
||||||
|
assert recipe.working_time == 15
|
||||||
|
assert recipe.waiting_time == 15
|
||||||
|
@ -1,27 +1,25 @@
|
|||||||
from cookbook.models import Storage
|
from cookbook.models import Storage
|
||||||
from cookbook.tests.views.test_views import TestViews
|
|
||||||
from django.contrib import auth
|
from django.contrib import auth
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
class TestEditsRecipe(TestViews):
|
@pytest.fixture
|
||||||
|
def storage_obj(a1_s1, space_1):
|
||||||
def setUp(self):
|
return Storage.objects.create(
|
||||||
super(TestEditsRecipe, self).setUp()
|
|
||||||
|
|
||||||
self.storage = Storage.objects.create(
|
|
||||||
name='TestStorage',
|
name='TestStorage',
|
||||||
method=Storage.DROPBOX,
|
method=Storage.DROPBOX,
|
||||||
created_by=auth.get_user(self.admin_client_1),
|
created_by=auth.get_user(a1_s1),
|
||||||
token='test',
|
token='test',
|
||||||
username='test',
|
username='test',
|
||||||
password='test',
|
password='test',
|
||||||
|
space=space_1,
|
||||||
)
|
)
|
||||||
self.url = reverse('edit_storage', args=[self.storage.pk])
|
|
||||||
|
|
||||||
def test_edit_storage(self):
|
|
||||||
r = self.admin_client_1.post(
|
def test_edit_storage(storage_obj, a1_s1, a1_s2):
|
||||||
self.url,
|
r = a1_s1.post(
|
||||||
|
reverse('edit_storage', args={storage_obj.pk}),
|
||||||
{
|
{
|
||||||
'name': 'NewStorage',
|
'name': 'NewStorage',
|
||||||
'password': '1234_pw',
|
'password': '1234_pw',
|
||||||
@ -29,40 +27,32 @@ class TestEditsRecipe(TestViews):
|
|||||||
'method': Storage.DROPBOX
|
'method': Storage.DROPBOX
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.storage.refresh_from_db()
|
storage_obj.refresh_from_db()
|
||||||
self.assertEqual(self.storage.password, '1234_pw')
|
assert r.status_code == 200
|
||||||
self.assertEqual(self.storage.token, '1234_token')
|
assert storage_obj.password == '1234_pw'
|
||||||
|
assert storage_obj.token == '1234_token'
|
||||||
|
|
||||||
r = self.admin_client_1.post(
|
r = a1_s2.post(
|
||||||
self.url,
|
reverse('edit_storage', args={storage_obj.pk}),
|
||||||
{
|
{
|
||||||
'name': 'NewStorage',
|
'name': 'NewStorage',
|
||||||
'password': '1234_pw',
|
'password': '1234_pw',
|
||||||
'token': '1234_token',
|
'token': '1234_token',
|
||||||
'method': 'not_a_valid_method'
|
'method': Storage.DROPBOX
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.assertFormError(
|
assert r.status_code == 404
|
||||||
r,
|
|
||||||
'form',
|
|
||||||
'method',
|
|
||||||
[
|
|
||||||
'Select a valid choice. not_a_valid_method is not one of the available choices.' # noqa: E501
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_edit_storage_permissions(self):
|
|
||||||
r = self.anonymous_client.get(self.url)
|
|
||||||
self.assertEqual(r.status_code, 302)
|
|
||||||
|
|
||||||
r = self.guest_client_1.get(self.url)
|
@pytest.mark.parametrize("arg", [
|
||||||
self.assertEqual(r.status_code, 302)
|
['a_u', 302],
|
||||||
|
['g1_s1', 302],
|
||||||
r = self.user_client_1.get(self.url)
|
['u1_s1', 302],
|
||||||
self.assertEqual(r.status_code, 302)
|
['a1_s1', 200],
|
||||||
|
['g1_s2', 302],
|
||||||
r = self.admin_client_1.get(self.url)
|
['u1_s2', 302],
|
||||||
self.assertEqual(r.status_code, 200)
|
['a1_s2', 404],
|
||||||
|
])
|
||||||
r = self.superuser_client.get(self.url)
|
def test_view_permission(arg, request, storage_obj):
|
||||||
self.assertEqual(r.status_code, 200)
|
c = request.getfixturevalue(arg[0])
|
||||||
|
assert c.get(reverse('edit_storage', args={storage_obj.pk})).status_code == arg[1]
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
from django.test import utils
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
# disables scoping error in all queries used inside the test FUNCTIONS
|
||||||
|
# FIXTURES need to have their own scopes_disabled!!
|
||||||
|
# This is done by hook pytest_fixture_setup in conftest.py for all non yield fixtures
|
||||||
|
utils.setup_databases = scopes_disabled()(utils.setup_databases)
|
@ -1,94 +0,0 @@
|
|||||||
import json
|
|
||||||
|
|
||||||
from cookbook.helper.ingredient_parser import parse
|
|
||||||
from cookbook.helper.recipe_url_import import get_from_html
|
|
||||||
from cookbook.tests.test_setup import TestBase
|
|
||||||
|
|
||||||
|
|
||||||
class TestEditsRecipe(TestBase):
|
|
||||||
|
|
||||||
# flake8: noqa
|
|
||||||
def test_ld_json(self):
|
|
||||||
test_list = [
|
|
||||||
{'file': 'cookbook/tests/resources/websites/ld_json_1.html', 'result_length': 3237},
|
|
||||||
{'file': 'cookbook/tests/resources/websites/ld_json_2.html', 'result_length': 1509},
|
|
||||||
{'file': 'cookbook/tests/resources/websites/ld_json_3.html', 'result_length': 1629},
|
|
||||||
{'file': 'cookbook/tests/resources/websites/ld_json_4.html', 'result_length': 1744},
|
|
||||||
{'file': 'cookbook/tests/resources/websites/ld_json_itemList.html', 'result_length': 3206},
|
|
||||||
{'file': 'cookbook/tests/resources/websites/ld_json_multiple.html', 'result_length': 1621},
|
|
||||||
{'file': 'cookbook/tests/resources/websites/micro_data_1.html', 'result_length': 1079},
|
|
||||||
{'file': 'cookbook/tests/resources/websites/micro_data_2.html', 'result_length': 1438},
|
|
||||||
{'file': 'cookbook/tests/resources/websites/micro_data_3.html', 'result_length': 1148},
|
|
||||||
{'file': 'cookbook/tests/resources/websites/micro_data_4.html', 'result_length': 4396},
|
|
||||||
]
|
|
||||||
|
|
||||||
for test in test_list:
|
|
||||||
with open(test['file'], 'rb') as file:
|
|
||||||
print(f'Testing {test["file"]} expecting length {test["result_length"]}')
|
|
||||||
parsed_content = json.loads(get_from_html(file.read(), 'test_url').content)
|
|
||||||
self.assertEqual(len(str(parsed_content)), test['result_length'])
|
|
||||||
file.close()
|
|
||||||
|
|
||||||
def test_ingredient_parser(self):
|
|
||||||
expectations = {
|
|
||||||
"2¼ l Wasser": (2.25, "l", "Wasser", ""),
|
|
||||||
"2¼l Wasser": (2.25, "l", "Wasser", ""),
|
|
||||||
"¼ l Wasser": (0.25, "l", "Wasser", ""),
|
|
||||||
"3l Wasser": (3, "l", "Wasser", ""),
|
|
||||||
"4 l Wasser": (4, "l", "Wasser", ""),
|
|
||||||
"½l Wasser": (0.5, "l", "Wasser", ""),
|
|
||||||
"⅛ Liter Sauerrahm": (0.125, "Liter", "Sauerrahm", ""),
|
|
||||||
"5 Zwiebeln": (5, "", "Zwiebeln", ""),
|
|
||||||
"3 Zwiebeln, gehackt": (3, "", "Zwiebeln", "gehackt"),
|
|
||||||
"5 Zwiebeln (gehackt)": (5, "", "Zwiebeln", "gehackt"),
|
|
||||||
"1 Zwiebel(n)": (1, "", "Zwiebel(n)", ""),
|
|
||||||
"4 1/2 Zwiebeln": (4.5, "", "Zwiebeln", ""),
|
|
||||||
"4 ½ Zwiebeln": (4.5, "", "Zwiebeln", ""),
|
|
||||||
"1/2 EL Mehl": (0.5, "EL", "Mehl", ""),
|
|
||||||
"1/2 Zwiebel": (0.5, "", "Zwiebel", ""),
|
|
||||||
"1/5g Mehl, gesiebt": (0.2, "g", "Mehl", "gesiebt"),
|
|
||||||
"1/2 Zitrone, ausgepresst": (0.5, "", "Zitrone", "ausgepresst"),
|
|
||||||
"etwas Mehl": (0, "", "etwas Mehl", ""),
|
|
||||||
"Öl zum Anbraten": (0, "", "Öl zum Anbraten", ""),
|
|
||||||
"n. B. Knoblauch, zerdrückt": (0, "", "n. B. Knoblauch", "zerdrückt"),
|
|
||||||
"Kräuter, mediterrane (Oregano, Rosmarin, Basilikum)": (
|
|
||||||
0, "", "Kräuter, mediterrane", "Oregano, Rosmarin, Basilikum"),
|
|
||||||
"600 g Kürbisfleisch (Hokkaido), geschält, entkernt und geraspelt": (
|
|
||||||
600, "g", "Kürbisfleisch (Hokkaido)", "geschält, entkernt und geraspelt"),
|
|
||||||
"Muskat": (0, "", "Muskat", ""),
|
|
||||||
"200 g Mehl, glattes": (200, "g", "Mehl", "glattes"),
|
|
||||||
"1 Ei(er)": (1, "", "Ei(er)", ""),
|
|
||||||
"1 Prise(n) Salz": (1, "Prise(n)", "Salz", ""),
|
|
||||||
"etwas Wasser, lauwarmes": (0, "", "etwas Wasser", "lauwarmes"),
|
|
||||||
"Strudelblätter, fertige, für zwei Strudel": (0, "", "Strudelblätter", "fertige, für zwei Strudel"),
|
|
||||||
"barrel-aged Bourbon": (0, "", "barrel-aged Bourbon", ""),
|
|
||||||
"golden syrup": (0, "", "golden syrup", ""),
|
|
||||||
"unsalted butter, for greasing": (0, "", "unsalted butter", "for greasing"),
|
|
||||||
"unsalted butter , for greasing": (0, "", "unsalted butter", "for greasing"), # trim
|
|
||||||
"1 small sprig of fresh rosemary": (1, "small", "sprig of fresh rosemary", ""),
|
|
||||||
# does not always work perfectly!
|
|
||||||
"75 g fresh breadcrumbs": (75, "g", "fresh breadcrumbs", ""),
|
|
||||||
"4 acorn squash , or onion squash (600-800g)": (4, "acorn", "squash , or onion squash", "600-800g"),
|
|
||||||
"1 x 250 g packet of cooked mixed grains , such as spelt and wild rice": (
|
|
||||||
1, "x", "250 g packet of cooked mixed grains", "such as spelt and wild rice"),
|
|
||||||
"1 big bunch of fresh mint , (60g)": (1, "big", "bunch of fresh mint ,", "60g"),
|
|
||||||
"1 large red onion": (1, "large", "red onion", ""),
|
|
||||||
# "2-3 TL Curry": (), # idk what it should use here either
|
|
||||||
"1 Zwiebel gehackt": (1, "Zwiebel", "gehackt", ""),
|
|
||||||
"1 EL Kokosöl": (1, "EL", "Kokosöl", ""),
|
|
||||||
"0.5 paket jäst (à 50 g)": (0.5, "paket", "jäst", "à 50 g"),
|
|
||||||
"ägg": (0, "", "ägg", ""),
|
|
||||||
"50 g smör eller margarin": (50, "g", "smör eller margarin", ""),
|
|
||||||
"3,5 l Wasser": (3.5, "l", "Wasser", ""),
|
|
||||||
"3.5 l Wasser": (3.5, "l", "Wasser", ""),
|
|
||||||
"400 g Karotte(n)": (400, "g", "Karotte(n)", "")
|
|
||||||
}
|
|
||||||
# for German you could say that if an ingredient does not have
|
|
||||||
# an amount # and it starts with a lowercase letter, then that
|
|
||||||
# is a unit ("etwas", "evtl.") does not apply to English tho
|
|
||||||
|
|
||||||
count = 0
|
|
||||||
for key, val in expectations.items():
|
|
||||||
count += 1
|
|
||||||
parsed = parse(key)
|
|
||||||
self.assertEqual(val, parsed)
|
|
66
cookbook/tests/other/test_ingredient_parser.py
Normal file
66
cookbook/tests/other/test_ingredient_parser.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
from cookbook.helper.ingredient_parser import parse
|
||||||
|
|
||||||
|
|
||||||
|
def test_ingredient_parser():
|
||||||
|
expectations = {
|
||||||
|
"2¼ l Wasser": (2.25, "l", "Wasser", ""),
|
||||||
|
"2¼l Wasser": (2.25, "l", "Wasser", ""),
|
||||||
|
"¼ l Wasser": (0.25, "l", "Wasser", ""),
|
||||||
|
"3l Wasser": (3, "l", "Wasser", ""),
|
||||||
|
"4 l Wasser": (4, "l", "Wasser", ""),
|
||||||
|
"½l Wasser": (0.5, "l", "Wasser", ""),
|
||||||
|
"⅛ Liter Sauerrahm": (0.125, "Liter", "Sauerrahm", ""),
|
||||||
|
"5 Zwiebeln": (5, "", "Zwiebeln", ""),
|
||||||
|
"3 Zwiebeln, gehackt": (3, "", "Zwiebeln", "gehackt"),
|
||||||
|
"5 Zwiebeln (gehackt)": (5, "", "Zwiebeln", "gehackt"),
|
||||||
|
"1 Zwiebel(n)": (1, "", "Zwiebel(n)", ""),
|
||||||
|
"4 1/2 Zwiebeln": (4.5, "", "Zwiebeln", ""),
|
||||||
|
"4 ½ Zwiebeln": (4.5, "", "Zwiebeln", ""),
|
||||||
|
"1/2 EL Mehl": (0.5, "EL", "Mehl", ""),
|
||||||
|
"1/2 Zwiebel": (0.5, "", "Zwiebel", ""),
|
||||||
|
"1/5g Mehl, gesiebt": (0.2, "g", "Mehl", "gesiebt"),
|
||||||
|
"1/2 Zitrone, ausgepresst": (0.5, "", "Zitrone", "ausgepresst"),
|
||||||
|
"etwas Mehl": (0, "", "etwas Mehl", ""),
|
||||||
|
"Öl zum Anbraten": (0, "", "Öl zum Anbraten", ""),
|
||||||
|
"n. B. Knoblauch, zerdrückt": (0, "", "n. B. Knoblauch", "zerdrückt"),
|
||||||
|
"Kräuter, mediterrane (Oregano, Rosmarin, Basilikum)": (
|
||||||
|
0, "", "Kräuter, mediterrane", "Oregano, Rosmarin, Basilikum"),
|
||||||
|
"600 g Kürbisfleisch (Hokkaido), geschält, entkernt und geraspelt": (
|
||||||
|
600, "g", "Kürbisfleisch (Hokkaido)", "geschält, entkernt und geraspelt"),
|
||||||
|
"Muskat": (0, "", "Muskat", ""),
|
||||||
|
"200 g Mehl, glattes": (200, "g", "Mehl", "glattes"),
|
||||||
|
"1 Ei(er)": (1, "", "Ei(er)", ""),
|
||||||
|
"1 Prise(n) Salz": (1, "Prise(n)", "Salz", ""),
|
||||||
|
"etwas Wasser, lauwarmes": (0, "", "etwas Wasser", "lauwarmes"),
|
||||||
|
"Strudelblätter, fertige, für zwei Strudel": (0, "", "Strudelblätter", "fertige, für zwei Strudel"),
|
||||||
|
"barrel-aged Bourbon": (0, "", "barrel-aged Bourbon", ""),
|
||||||
|
"golden syrup": (0, "", "golden syrup", ""),
|
||||||
|
"unsalted butter, for greasing": (0, "", "unsalted butter", "for greasing"),
|
||||||
|
"unsalted butter , for greasing": (0, "", "unsalted butter", "for greasing"), # trim
|
||||||
|
"1 small sprig of fresh rosemary": (1, "small", "sprig of fresh rosemary", ""),
|
||||||
|
# does not always work perfectly!
|
||||||
|
"75 g fresh breadcrumbs": (75, "g", "fresh breadcrumbs", ""),
|
||||||
|
"4 acorn squash , or onion squash (600-800g)": (4, "acorn", "squash , or onion squash", "600-800g"),
|
||||||
|
"1 x 250 g packet of cooked mixed grains , such as spelt and wild rice": (
|
||||||
|
1, "x", "250 g packet of cooked mixed grains", "such as spelt and wild rice"),
|
||||||
|
"1 big bunch of fresh mint , (60g)": (1, "big", "bunch of fresh mint ,", "60g"),
|
||||||
|
"1 large red onion": (1, "large", "red onion", ""),
|
||||||
|
# "2-3 TL Curry": (), # idk what it should use here either
|
||||||
|
"1 Zwiebel gehackt": (1, "Zwiebel", "gehackt", ""),
|
||||||
|
"1 EL Kokosöl": (1, "EL", "Kokosöl", ""),
|
||||||
|
"0.5 paket jäst (à 50 g)": (0.5, "paket", "jäst", "à 50 g"),
|
||||||
|
"ägg": (0, "", "ägg", ""),
|
||||||
|
"50 g smör eller margarin": (50, "g", "smör eller margarin", ""),
|
||||||
|
"3,5 l Wasser": (3.5, "l", "Wasser", ""),
|
||||||
|
"3.5 l Wasser": (3.5, "l", "Wasser", ""),
|
||||||
|
"400 g Karotte(n)": (400, "g", "Karotte(n)", "")
|
||||||
|
}
|
||||||
|
# for German you could say that if an ingredient does not have
|
||||||
|
# an amount # and it starts with a lowercase letter, then that
|
||||||
|
# is a unit ("etwas", "evtl.") does not apply to English tho
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for key, val in expectations.items():
|
||||||
|
count += 1
|
||||||
|
parsed = parse(key)
|
||||||
|
assert val == parsed
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user