Merge remote-tracking branch 'upstream/develop' into database-url

This commit is contained in:
tourn 2021-05-15 11:19:52 +02:00
commit 7650edfdc8
289 changed files with 85075 additions and 11456 deletions

View File

@ -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
DB_ENGINE=django.db.backends.postgresql
# DB_OPTIONS= {} # e.g. {"sslmode":"require"} to enable ssl
POSTGRES_HOST=db_recipes
POSTGRES_PORT=5432
POSTGRES_USER=djangouser
@ -57,3 +58,10 @@ REVERSE_PROXY_AUTH=0
# see docs for more information https://vabene1111.github.io/recipes/features/authentication/
# SOCIAL_PROVIDERS = allauth.socialaccount.providers.github, allauth.socialaccount.providers.nextcloud,
# Should a newly created user from a social provider get assigned to the default space and given permission by default ?
# ATTENTION: This feature might be deprecated in favor of a space join and public viewing system in the future
# default 0 (false), when 1 (true) users will be assigned space and group
# SOCIAL_DEFAULT_ACCESS = 1
# if SOCIAL_DEFAULT_ACCESS is used, which group should be added
# SOCIAL_DEFAULT_GROUP=guest

View File

@ -6,14 +6,16 @@ labels: setup issue
assignees: ''
---
### Version
Please provide your current version (can be found on the system page since v0.8.4)
Version:
### Issue
## Issue
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`
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
```
### 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
View 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
```

View File

@ -9,14 +9,14 @@ jobs:
strategy:
max-parallel: 4
matrix:
python-version: [3.8]
python-version: [3.9]
steps:
- uses: actions/checkout@v1
- name: Set up Python 3.8
- name: Set up Python 3.9
uses: actions/setup-python@v1
with:
python-version: 3.8
python-version: 3.9
- name: Install dependencies
run: |
python -m pip install --upgrade pip
@ -25,4 +25,4 @@ jobs:
python3 manage.py collectstatic_js_reverse
- name: Django Testing project
run: |
python3 manage.py test
pytest

View File

@ -12,40 +12,42 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
# Override language selection by uncommenting this and choosing your languages
# with:
# languages: go, javascript, csharp, python, cpp, java
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
# - name: Autobuild
# uses: github/codeql-action/autobuild@v1
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
# Override language selection by uncommenting this and choosing your languages
with:
languages: python, javascript
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
# - name: Autobuild
# uses: github/codeql-action/autobuild@v1
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
#- run: |
# make bootstrap
# make release
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
with:
languages: javascript, python

1
.gitignore vendored
View File

@ -78,3 +78,4 @@ postgresql/
/docker-compose.override.yml
vue/node_modules
.vscode/

View File

@ -7,6 +7,7 @@
<w>gunicorn</w>
<w>ical</w>
<w>mealie</w>
<w>pepperplate</w>
<w>safron</w>
<w>traefik</w>
</words>

View File

@ -18,7 +18,7 @@
<excludeFolder url="file://$MODULE_DIR$/staticfiles" />
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.8 (recipes)" jdkType="Python SDK" />
<orderEntry type="jdk" jdkName="Python 3.9 (recipes)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="TemplatesService">
@ -29,4 +29,7 @@
</list>
</option>
</component>
<component name="TestRunnerService">
<option name="PROJECT_TEST_RUNNER" value="pytest" />
</component>
</module>

View File

@ -1,18 +1,28 @@
FROM python:3.8-alpine
FROM python:3.9-alpine3.12
RUN apk add --no-cache postgresql-libs gettext zlib libjpeg libxml2-dev libxslt-dev
#Install all dependencies.
RUN apk add --no-cache postgresql-libs gettext zlib libjpeg libxml2-dev libxslt-dev py-cryptography
#Print all logs without buffering it.
ENV PYTHONUNBUFFERED 1
#This port will be used by gunicorn.
EXPOSE 8080
#Create app dir and install requirements.
RUN mkdir /opt/recipes
WORKDIR /opt/recipes
COPY . ./
RUN chmod +x boot.sh
COPY requirements.txt ./
RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-dev jpeg-dev libressl-dev libffi-dev cargo && \
python -m venv venv && \
/opt/recipes/venv/bin/python -m pip install --upgrade pip && \
venv/bin/pip install wheel==0.36.2 && \
venv/bin/pip install -r requirements.txt --no-cache-dir &&\
apk --purge del .build-deps
#Copy project and execute it.
COPY . ./
RUN chmod +x boot.sh
ENTRYPOINT ["/opt/recipes/boot.sh"]

View File

@ -1,25 +1,36 @@
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,
MealPlan, MealType, NutritionInformation, Recipe,
RecipeBook, RecipeBookEntry, RecipeImport, ShareLink,
ShoppingList, ShoppingListEntry, ShoppingListRecipe,
Space, Step, Storage, Sync, SyncLog, Unit, UserPreference,
ViewLog, Supermarket, SupermarketCategory, SupermarketCategoryRelation)
ViewLog, Supermarket, SupermarketCategory, SupermarketCategoryRelation,
ImportLog, TelegramBot, BookmarkletImport)
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):
list_display = ('name', 'message')
list_display = ('name', 'created_by', 'message')
admin.site.register(Space, SpaceAdmin)
class UserPreferenceAdmin(admin.ModelAdmin):
list_display = (
'name', 'theme', 'nav_color',
'default_page', 'search_style', 'comments'
)
list_display = ('name', 'space', 'theme', 'nav_color', 'default_page', 'search_style',)
@staticmethod
def name(obj):
@ -203,3 +214,24 @@ class NutritionInformationAdmin(admin.ModelAdmin):
admin.site.register(NutritionInformation, NutritionInformationAdmin)
class ImportLogAdmin(admin.ModelAdmin):
list_display = ('id', 'type', 'running', 'created_by', 'created_at',)
admin.site.register(ImportLog, ImportLogAdmin)
class TelegramBotAdmin(admin.ModelAdmin):
list_display = ('id', 'name', 'created_by',)
admin.site.register(TelegramBot, TelegramBotAdmin)
class BookmarkletImportAdmin(admin.ModelAdmin):
list_display = ('id', 'url', 'created_by', 'created_at',)
admin.site.register(BookmarkletImport, BookmarkletImportAdmin)

View File

@ -3,77 +3,81 @@ from django.conf import settings
from django.contrib.postgres.search import TrigramSimilarity
from django.db.models import Q
from django.utils.translation import gettext as _
from django_scopes import scopes_disabled
from cookbook.forms import MultiSelectWidget
from cookbook.models import Food, Keyword, Recipe, ShoppingList
with scopes_disabled():
class RecipeFilter(django_filters.FilterSet):
name = django_filters.CharFilter(method='filter_name')
keywords = django_filters.ModelMultipleChoiceFilter(
queryset=Keyword.objects.none(),
widget=MultiSelectWidget,
method='filter_keywords'
)
foods = django_filters.ModelMultipleChoiceFilter(
queryset=Food.objects.none(),
widget=MultiSelectWidget,
method='filter_foods',
label=_('Ingredients')
)
class RecipeFilter(django_filters.FilterSet):
name = django_filters.CharFilter(method='filter_name')
keywords = django_filters.ModelMultipleChoiceFilter(
queryset=Keyword.objects.all(),
widget=MultiSelectWidget,
method='filter_keywords'
)
foods = django_filters.ModelMultipleChoiceFilter(
queryset=Food.objects.all(),
widget=MultiSelectWidget,
method='filter_foods',
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
def filter_keywords(queryset, name, value):
if not name == 'keywords':
@staticmethod
def filter_keywords(queryset, name, value):
if not name == 'keywords':
return queryset
for x in value:
queryset = queryset.filter(keywords=x)
return queryset
for x in value:
queryset = queryset.filter(keywords=x)
return queryset
@staticmethod
def filter_foods(queryset, name, value):
if not name == 'foods':
@staticmethod
def filter_foods(queryset, name, value):
if not name == 'foods':
return queryset
for x in value:
queryset = queryset.filter(steps__ingredients__food__name=x).distinct()
return queryset
for x in value:
queryset = queryset.filter(
steps__ingredients__food__name=x
).distinct()
return queryset
@staticmethod
def filter_name(queryset, name, value):
if not name == 'name':
@staticmethod
def filter_name(queryset, name, value):
if not name == 'name':
return queryset
if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2',
'django.db.backends.postgresql']:
queryset = queryset.annotate(similarity=TrigramSimilarity('name', value), ).filter(
Q(similarity__gt=0.1) | Q(name__unaccent__icontains=value)).order_by('-similarity')
else:
queryset = queryset.filter(name__icontains=value)
return queryset
if settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql_psycopg2': # noqa: E501
queryset = queryset \
.annotate(similarity=TrigramSimilarity('name', value), ) \
.filter(Q(similarity__gt=0.1) | Q(name__unaccent__icontains=value)) \
.order_by('-similarity')
else:
queryset = queryset.filter(name__icontains=value)
return queryset
class Meta:
model = Recipe
fields = ['name', 'keywords', 'foods', 'internal']
class Meta:
model = Recipe
fields = ['name', 'keywords', 'foods', 'internal']
class IngredientFilter(django_filters.FilterSet):
name = django_filters.CharFilter(lookup_expr='icontains')
class FoodFilter(django_filters.FilterSet):
name = django_filters.CharFilter(lookup_expr='icontains')
class Meta:
model = Food
fields = ['name']
class Meta:
model = Food
fields = ['name']
class ShoppingListFilter(django_filters.FilterSet):
class ShoppingListFilter(django_filters.FilterSet):
def __init__(self, data=None, *args, **kwargs):
if data is not None:
data = data.copy()
data.setdefault("finished", False)
super(ShoppingListFilter, self).__init__(data, *args, **kwargs)
def __init__(self, data=None, *args, **kwargs):
if data is not None:
data = data.copy()
data.setdefault("finished", False)
super().__init__(data, *args, **kwargs)
class Meta:
model = ShoppingList
fields = ['finished']
class Meta:
model = ShoppingList
fields = ['finished']

View File

@ -1,11 +1,12 @@
from django import forms
from django.forms import widgets
from django.utils.translation import gettext_lazy as _
from django_scopes.forms import SafeModelChoiceField, SafeModelMultipleChoiceField
from emoji_picker.widgets import EmojiPickerTextInput
from .models import (Comment, Food, InviteLink, Keyword, MealPlan, Recipe,
RecipeBook, RecipeBookEntry, Storage, Sync, Unit, User,
UserPreference)
UserPreference, SupermarketCategory, MealType, Space)
class SelectWidget(widgets.Select):
@ -74,18 +75,18 @@ class UserNameForm(forms.ModelForm):
class ExternalRecipeForm(forms.ModelForm):
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)
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:
model = Recipe
fields = (
'name', 'keywords', 'description', 'servings', 'working_time', 'waiting_time',
'file_path', 'storage', 'file_uid'
'name', 'description', 'servings', 'working_time', 'waiting_time',
'file_path', 'file_uid', 'keywords'
)
labels = {
@ -97,38 +98,9 @@ class ExternalRecipeForm(forms.ModelForm):
'file_uid': _('Storage UID'),
}
widgets = {'keywords': MultiSelectWidget}
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'),
field_classes = {
'keywords': SafeModelMultipleChoiceField,
}
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):
@ -138,54 +110,81 @@ class ImportExportBase(forms.Form):
MEALIE = 'MEALIE'
CHOWDOWN = 'CHOWDOWN'
SAFRON = 'SAFRON'
CHEFTAP = 'CHEFTAP'
PEPPERPLATE = 'PEPPERPLATE'
RECIPESAGE = 'RECIPESAGE'
DOMESTICA = 'DOMESTICA'
MEALMASTER = 'MEALMASTER'
REZKONV = 'REZKONV'
type = forms.ChoiceField(choices=(
(DEFAULT, _('Default')), (PAPRIKA, 'Paprika'), (NEXTCLOUD, 'Nextcloud Cookbook'),
(MEALIE, 'Mealie'), (CHOWDOWN, 'Chowdown'), (SAFRON, 'Safron'),
(MEALIE, 'Mealie'), (CHOWDOWN, 'Chowdown'), (SAFRON, 'Safron'), (CHEFTAP, 'ChefTap'),
(PEPPERPLATE, 'Pepperplate'), (RECIPESAGE, 'Recipe Sage'), (DOMESTICA, 'Domestica'),
(MEALMASTER, 'MealMaster'), (REZKONV, 'RezKonv'),
))
class ImportForm(ImportExportBase):
files = forms.FileField(required=True, widget=forms.ClearableFileInput(attrs={'multiple': True}))
duplicates = forms.BooleanField(help_text=_('To prevent duplicates recipes with the same name as existing ones are ignored. Check this box to import everything.'), required=False)
class ExportForm(ImportExportBase):
recipes = forms.ModelMultipleChoiceField(queryset=Recipe.objects.filter(internal=True).all(), widget=MultiSelectWidget)
recipes = forms.ModelMultipleChoiceField(widget=MultiSelectWidget, queryset=Recipe.objects.none())
all = forms.BooleanField(required=False)
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):
prefix = 'unit'
new_unit = forms.ModelChoiceField(
queryset=Unit.objects.all(),
new_unit = SafeModelChoiceField(
queryset=Unit.objects.none(),
widget=SelectWidget,
label=_('New Unit'),
help_text=_('New unit that other gets replaced by.'),
)
old_unit = forms.ModelChoiceField(
queryset=Unit.objects.all(),
old_unit = SafeModelChoiceField(
queryset=Unit.objects.none(),
widget=SelectWidget,
label=_('Old Unit'),
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):
prefix = 'food'
new_food = forms.ModelChoiceField(
queryset=Food.objects.all(),
new_food = SafeModelChoiceField(
queryset=Food.objects.none(),
widget=SelectWidget,
label=_('New Food'),
help_text=_('New food that other gets replaced by.'),
)
old_food = forms.ModelChoiceField(
queryset=Food.objects.all(),
old_food = SafeModelChoiceField(
queryset=Food.objects.none(),
widget=SelectWidget,
label=_('Old Food'),
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):
prefix = 'comment'
@ -210,11 +209,23 @@ class KeywordForm(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:
model = Food
fields = ('name', 'description', 'ignore_shopping', 'recipe', 'supermarket_category')
widgets = {'recipe': SelectWidget}
field_classes = {
'recipe': SafeModelChoiceField,
'supermarket_category': SafeModelChoiceField,
}
class StorageForm(forms.ModelForm):
username = forms.CharField(
@ -222,18 +233,16 @@ class StorageForm(forms.ModelForm):
required=False
)
password = forms.CharField(
widget=forms.TextInput(
attrs={'autocomplete': 'new-password', 'type': 'password'}
),
widget=forms.TextInput(attrs={'autocomplete': 'new-password', 'type': 'password'}),
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(
widget=forms.TextInput(
attrs={'autocomplete': 'new-password', 'type': 'password'}
),
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:
@ -241,34 +250,63 @@ class StorageForm(forms.ModelForm):
fields = ('name', 'method', 'username', 'password', 'token', 'url', 'path')
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):
prefix = 'bookmark'
def __init__(self, *args, **kwargs):
space = kwargs.pop('space')
super().__init__(*args, **kwargs)
self.fields['book'].queryset = RecipeBook.objects.filter(space=space).all()
class Meta:
model = RecipeBookEntry
fields = ('book',)
field_classes = {
'book': SafeModelChoiceField,
}
class SyncForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
space = kwargs.pop('space')
super().__init__(*args, **kwargs)
self.fields['storage'].queryset = Storage.objects.filter(space=space).all()
class Meta:
model = Sync
fields = ('storage', 'path', 'active')
field_classes = {
'storage': SafeModelChoiceField,
}
class BatchEditForm(forms.Form):
search = forms.CharField(label=_('Search String'))
keywords = forms.ModelMultipleChoiceField(
queryset=Keyword.objects.all().order_by('id'),
queryset=Keyword.objects.none(),
required=False,
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):
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:
model = Recipe
fields = ('name', 'keywords', 'file_path', 'file_uid')
@ -280,16 +318,33 @@ class ImportRecipeForm(forms.ModelForm):
'file_uid': _('File ID'),
}
widgets = {'keywords': MultiSelectWidget}
field_classes = {
'keywords': SafeModelChoiceField,
}
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:
model = RecipeBook
fields = ('name', 'icon', 'description', 'shared')
widgets = {'icon': EmojiPickerTextInput, 'shared': MultiSelectWidget}
field_classes = {
'shared': SafeModelMultipleChoiceField,
}
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):
cleaned_data = super(MealPlanForm, self).clean()
@ -318,15 +373,28 @@ class MealPlanForm(forms.ModelForm):
'date': DateWidget,
'shared': MultiSelectWidget
}
field_classes = {
'recipe': SafeModelChoiceField,
'meal_type': SafeModelChoiceField,
'shared': SafeModelMultipleChoiceField,
}
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:
model = InviteLink
fields = ('username', 'group', 'valid_until')
fields = ('username', 'group', 'valid_until', 'space')
help_texts = {
'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):

View 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)

View File

@ -10,7 +10,7 @@ class BaseAutocomplete(autocomplete.Select2QuerySetView):
if not self.request.user.is_authenticated:
return self.model.objects.none()
qs = self.model.objects.all()
qs = self.model.objects.filter(space=self.request.space).all()
if self.q:
qs = qs.filter(name__icontains=self.q)

View File

@ -1,6 +1,8 @@
import string
import unicodedata
from cookbook.models import Unit, Food
def parse_fraction(x):
if len(x) == 1 and 'fraction' in unicodedata.decomposition(x):
@ -157,3 +159,18 @@ def parse(x):
except ValueError:
ingredient = ' '.join(tokens[1:])
return amount, unit.strip(), ingredient.strip(), note.strip()
# small utility functions to prevent emtpy unit/food creation
def get_unit(unit, space):
if len(unit) > 0:
u, created = Unit.objects.get_or_create(name=unit, space=space)
return u
return None
def get_food(food, space):
if len(food) > 0:
f, created = Food.objects.get_or_create(name=food, space=space)
return f
return None

View File

@ -19,7 +19,8 @@ class StyleTreeprocessor(Treeprocessor):
class MarkdownFormatExtension(markdown.Extension):
def extendMarkdown(self, md, md_globals):
# md_ globals deprecated - see here:
def extendMarkdown(self, md):
md.treeprocessors.register(
StyleTreeprocessor(),
'StyleTreeprocessor',

View File

@ -1,6 +1,9 @@
"""
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 django.contrib import messages
from django.contrib.auth.decorators import user_passes_test
@ -40,8 +43,7 @@ def has_group_permission(user, groups):
return False
groups_allowed = get_allowed_groups(groups)
if user.is_authenticated:
if (user.is_superuser
| bool(user.groups.filter(name__in=groups_allowed))):
if bool(user.groups.filter(name__in=groups_allowed)):
return True
return False
@ -56,19 +58,12 @@ def is_object_owner(user, obj):
:param obj any object that should be tested
: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:
return False
if user.is_superuser:
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):
try:
return obj.get_owner() == user
return False
except:
return False
def is_object_shared(user, obj):
@ -84,9 +79,7 @@ def is_object_shared(user, obj):
# share checks for relevant objects
if not user.is_authenticated:
return False
if user.is_superuser:
return True
return user in obj.shared.all()
return user in obj.get_shared()
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
"""
try:
return (
True
if ShareLink.objects.filter(recipe=recipe, uuid=share).exists()
else False
)
return True if ShareLink.objects.filter(recipe=recipe, uuid=share).exists() else False
except ValidationError:
return False
@ -119,7 +108,7 @@ def group_required(*groups_required):
def in_groups(u):
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):
@ -131,13 +120,17 @@ class GroupRequiredMixin(object):
def dispatch(self, request, *args, **kwargs):
if not has_group_permission(request.user, self.groups_required):
messages.add_message(
request,
messages.ERROR,
_('You do not have the required permissions to view this page!') # noqa: E501
)
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
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)
@ -145,25 +138,22 @@ class OwnerRequiredMixin(object):
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
messages.add_message(
request,
messages.ERROR,
_('You are not logged in and therefore cannot view this page!')
)
return HttpResponseRedirect(
reverse_lazy('account_login') + '?next=' + request.path
)
messages.add_message(request, messages.ERROR, _('You are not logged in and therefore cannot view this page!'))
return HttpResponseRedirect(reverse_lazy('account_login') + '?next=' + request.path)
else:
if not is_object_owner(request.user, self.get_object()):
messages.add_message(
request,
messages.ERROR,
_('You cannot interact with this object as it is not owned by you!') # noqa: E501
)
messages.add_message(request, messages.ERROR, _('You cannot interact with this object as it is not owned by you!'))
return HttpResponseRedirect(reverse('index'))
return super(OwnerRequiredMixin, self) \
.dispatch(request, *args, **kwargs)
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(OwnerRequiredMixin, self).dispatch(request, *args, **kwargs)
# Django Rest Framework Permission classes

View File

@ -0,0 +1,193 @@
import json
import re
from bs4 import BeautifulSoup
from bs4.element import Tag
from cookbook.helper import recipe_url_import as helper
from cookbook.helper.scrapers.scrapers import text_scraper
from json import JSONDecodeError
from recipe_scrapers._utils import get_host_name, normalize_string
from urllib.parse import unquote
def get_recipe_from_source(text, url, space):
def build_node(k, v):
if isinstance(v, dict):
node = {
'name': k,
'value': k,
'children': get_children_dict(v)
}
elif isinstance(v, list):
node = {
'name': k,
'value': k,
'children': get_children_list(v)
}
else:
node = {
'name': k + ": " + normalize_string(str(v)),
'value': normalize_string(str(v))
}
return node
def get_children_dict(children):
kid_list = []
for k, v in children.items():
kid_list.append(build_node(k, v))
return kid_list
def get_children_list(children):
kid_list = []
for kid in children:
if type(kid) == list:
node = {
'name': "unknown list",
'value': "unknown list",
'children': get_children_list(kid)
}
kid_list.append(node)
elif type(kid) == dict:
for k, v in kid.items():
kid_list.append(build_node(k, v))
else:
kid_list.append({
'name': normalize_string(str(kid)),
'value': normalize_string(str(kid))
})
return kid_list
recipe_json = {
'name': '',
'url': '',
'description': '',
'image': '',
'keywords': [],
'recipeIngredient': [],
'recipeInstructions': '',
'servings': '',
'prepTime': '',
'cookTime': ''
}
recipe_tree = []
parse_list = []
html_data = []
images = []
text = unquote(text)
try:
parse_list.append(remove_graph(json.loads(text)))
if not url and 'url' in parse_list[0]:
url = parse_list[0]['url']
scrape = text_scraper("<script type='application/ld+json'>" + text + "</script>", url=url)
except JSONDecodeError:
soup = BeautifulSoup(text, "html.parser")
html_data = get_from_html(soup)
images += get_images_from_source(soup, url)
for el in soup.find_all('script', type='application/ld+json'):
el = remove_graph(el)
if not url and 'url' in el:
url = el['url']
if type(el) == list:
for le in el:
parse_list.append(le)
elif type(el) == dict:
parse_list.append(el)
for el in soup.find_all(type='application/json'):
el = remove_graph(el)
if type(el) == list:
for le in el:
parse_list.append(le)
elif type(el) == dict:
parse_list.append(el)
scrape = text_scraper(text, url=url)
recipe_json = helper.get_from_scraper(scrape, space)
for el in parse_list:
temp_tree = []
if isinstance(el, Tag):
try:
el = json.loads(el.string)
except TypeError:
continue
for k, v in el.items():
if isinstance(v, dict):
node = {
'name': k,
'value': k,
'children': get_children_dict(v)
}
elif isinstance(v, list):
node = {
'name': k,
'value': k,
'children': get_children_list(v)
}
else:
node = {
'name': k + ": " + normalize_string(str(v)),
'value': normalize_string(str(v))
}
temp_tree.append(node)
if '@type' in el and el['@type'] == 'Recipe':
recipe_tree += [{'name': 'ld+json', 'children': temp_tree}]
else:
recipe_tree += [{'name': 'json', 'children': temp_tree}]
return recipe_json, recipe_tree, html_data, images
def get_from_html(soup):
INVISIBLE_ELEMS = ('style', 'script', 'head', 'title')
html = []
for s in soup.strings:
if ((s.parent.name not in INVISIBLE_ELEMS) and (len(s.strip()) > 0)):
html.append(s)
return html
def get_images_from_source(soup, url):
sources = ['src', 'srcset', 'data-src']
images = []
img_tags = soup.find_all('img')
if url:
site = get_host_name(url)
prot = url.split(':')[0]
urls = []
for img in img_tags:
for src in sources:
try:
urls.append(img[src])
except KeyError:
pass
for u in urls:
u = u.split('?')[0]
filename = re.search(r'/([\w_-]+[.](jpg|jpeg|gif|png))$', u)
if filename:
if (('http' not in u) and (url)):
# sometimes an image source can be relative
# if it is provide the base url
u = '{}://{}{}'.format(prot, site, u)
if 'http' in u:
images.append(u)
return images
def remove_graph(el):
# recipes type might be wrapped in @graph type
if isinstance(el, Tag):
try:
el = json.loads(el.string)
if '@graph' in el:
for x in el['@graph']:
if '@type' in x and x['@type'] == 'Recipe':
el = x
except TypeError:
pass
return el

View File

@ -0,0 +1,66 @@
from datetime import datetime, timedelta
from functools import reduce
from django.contrib.postgres.search import TrigramSimilarity
from django.db.models import Q
from cookbook.models import ViewLog
from recipes import settings
def search_recipes(request, queryset, params):
search_string = params.get('query', '')
search_keywords = params.getlist('keywords', [])
search_foods = params.getlist('foods', [])
search_books = params.getlist('books', [])
search_keywords_or = params.get('keywords_or', True)
search_foods_or = params.get('foods_or', True)
search_books_or = params.get('books_or', True)
search_internal = params.get('internal', None)
search_random = params.get('random', False)
search_last_viewed = int(params.get('last_viewed', 0))
if search_last_viewed > 0:
last_viewed_recipes = ViewLog.objects.filter(created_by=request.user, space=request.space, created_at__gte=datetime.now() - timedelta(days=14)).values_list('recipe__pk', flat=True).distinct()
return queryset.filter(pk__in=list(set(last_viewed_recipes))[-search_last_viewed:])
if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2',
'django.db.backends.postgresql']:
queryset = queryset.annotate(similarity=TrigramSimilarity('name', search_string), ).filter(
Q(similarity__gt=0.1) | Q(name__unaccent__icontains=search_string)).order_by('-similarity')
else:
queryset = queryset.filter(name__icontains=search_string)
if len(search_keywords) > 0:
if search_keywords_or == 'true':
queryset = queryset.filter(keywords__id__in=search_keywords)
else:
for k in search_keywords:
queryset = queryset.filter(keywords__id=k)
if len(search_foods) > 0:
if search_foods_or == 'true':
queryset = queryset.filter(steps__ingredients__food__id__in=search_foods)
else:
for k in search_foods:
queryset = queryset.filter(steps__ingredients__food__id=k)
if len(search_books) > 0:
if search_books_or == 'true':
queryset = queryset.filter(recipebookentry__book__id__in=search_books)
else:
for k in search_books:
queryset = queryset.filter(recipebookentry__book__id=k)
queryset = queryset.distinct()
if search_internal == 'true':
queryset = queryset.filter(internal=True)
if search_random == 'true':
queryset = queryset.order_by("?")
return queryset

View File

@ -1,236 +1,361 @@
import json
import random
import re
from json import JSONDecodeError
from isodate import parse_duration as iso_parse_duration
from isodate.isoerror import ISO8601Error
from recipe_scrapers._exceptions import ElementNotFoundInHtml
import microdata
from bs4 import BeautifulSoup
from cookbook.helper.ingredient_parser import parse as parse_ingredient
from cookbook.helper.ingredient_parser import parse as parse_single_ingredient
from cookbook.models import Keyword
from django.http import JsonResponse
from django.utils.dateparse import parse_duration
from django.utils.translation import gettext as _
from html import unescape
from recipe_scrapers._schemaorg import SchemaOrgException
from recipe_scrapers._utils import get_minutes
def get_from_html(html_text, url):
soup = BeautifulSoup(html_text, "html.parser")
# first try finding ld+json as its most common
for ld in soup.find_all('script', type='application/ld+json'):
def get_from_scraper(scrape, space):
# converting the scrape_me object to the existing json format based on ld+json
recipe_json = {}
try:
recipe_json['name'] = parse_name(scrape.title() or None)
except Exception:
recipe_json['name'] = None
if not recipe_json['name']:
try:
ld_json = json.loads(ld.string.replace('\n', ''))
if type(ld_json) != list:
ld_json = [ld_json]
for ld_json_item in ld_json:
# recipes type might be wrapped in @graph type
if '@graph' in ld_json_item:
for x in ld_json_item['@graph']:
if '@type' in x and x['@type'] == 'Recipe':
ld_json_item = x
if ('@type' in ld_json_item
and ld_json_item['@type'] == 'Recipe'):
return JsonResponse(find_recipe_json(ld_json_item, url))
except JSONDecodeError:
return JsonResponse(
{
'error': True,
'msg': _('The requested site provided malformed data and cannot be read.') # noqa: E501
},
status=400)
# now try to find microdata
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']):
return JsonResponse(find_recipe_json(md_json['properties'], url))
return JsonResponse(
{
'error': True,
'msg': _('The requested site does not provide any recognized data format to import the recipe from.') # noqa: E501
},
status=400)
def find_recipe_json(ld_json, url):
if type(ld_json['name']) == list:
try:
ld_json['name'] = ld_json['name'][0]
recipe_json['name'] = scrape.schema.data.get('name') or ''
except Exception:
ld_json['name'] = 'ERROR'
recipe_json['name'] = ''
# some sites use ingredients instead of recipeIngredients
if 'recipeIngredient' not in ld_json and 'ingredients' in ld_json:
ld_json['recipeIngredient'] = ld_json['ingredients']
try:
description = scrape.schema.data.get("description") or ''
except Exception:
description = ''
if 'recipeIngredient' in ld_json:
# some pages have comma separated ingredients in a single array entry
if (len(ld_json['recipeIngredient']) == 1
and len(ld_json['recipeIngredient'][0]) > 30):
ld_json['recipeIngredient'] = ld_json['recipeIngredient'][0].split(',') # noqa: E501
recipe_json['description'] = parse_description(description)
for x in ld_json['recipeIngredient']:
if '\n' in x:
ld_json['recipeIngredient'].remove(x)
for i in x.split('\n'):
ld_json['recipeIngredient'].insert(0, i)
try:
servings = scrape.yields() or None
except Exception:
servings = None
if not servings:
try:
servings = scrape.schema.data.get('recipeYield') or 1
except Exception:
servings = 1
if type(servings) != int:
try:
servings = int(re.findall(r'\b\d+\b', servings)[0])
except Exception:
servings = 1
recipe_json['servings'] = servings
try:
recipe_json['prepTime'] = get_minutes(scrape.schema.data.get("prepTime")) or 0
except Exception:
recipe_json['prepTime'] = 0
try:
recipe_json['cookTime'] = get_minutes(scrape.schema.data.get("cookTime")) or 0
except Exception:
recipe_json['cookTime'] = 0
if recipe_json['cookTime'] + recipe_json['prepTime'] == 0:
try:
recipe_json['prepTime'] = get_minutes(scrape.total_time()) or 0
except Exception:
try:
get_minutes(scrape.schema.data.get("totalTime")) or 0
except Exception:
pass
try:
recipe_json['image'] = parse_image(scrape.image()) or None
except Exception:
recipe_json['image'] = None
if not recipe_json['image']:
try:
recipe_json['image'] = parse_image(scrape.schema.data.get('image')) or ''
except Exception:
recipe_json['image'] = ''
keywords = []
try:
if scrape.schema.data.get("keywords"):
keywords += listify_keywords(scrape.schema.data.get("keywords"))
except Exception:
pass
try:
if scrape.schema.data.get('recipeCategory'):
keywords += listify_keywords(scrape.schema.data.get("recipeCategory"))
except Exception:
pass
try:
if scrape.schema.data.get('recipeCuisine'):
keywords += listify_keywords(scrape.schema.data.get("recipeCuisine"))
except Exception:
pass
try:
recipe_json['keywords'] = parse_keywords(list(set(map(str.casefold, keywords))), space)
except AttributeError:
recipe_json['keywords'] = keywords
try:
ingredients = []
for x in ld_json['recipeIngredient']:
if x.replace(' ', '') != '':
try:
amount, unit, ingredient, note = parse_ingredient(x)
if ingredient:
ingredients.append(
{
'amount': amount,
'unit': {
'text': unit,
'id': random.randrange(10000, 99999)
},
'ingredient': {
'text': ingredient,
'id': random.randrange(10000, 99999)
},
'note': note,
'original': x
}
)
except Exception:
for x in scrape.ingredients():
try:
amount, unit, ingredient, note = parse_single_ingredient(x)
if ingredient:
ingredients.append(
{
'amount': 0,
'amount': amount,
'unit': {
'text': '',
'text': unit,
'id': random.randrange(10000, 99999)
},
'ingredient': {
'text': x,
'text': ingredient,
'id': random.randrange(10000, 99999)
},
'note': '',
'note': note,
'original': x
}
)
ld_json['recipeIngredient'] = ingredients
else:
ld_json['recipeIngredient'] = []
if 'keywords' in ld_json:
keywords = []
# keywords as string
if type(ld_json['keywords']) == str:
ld_json['keywords'] = ld_json['keywords'].split(',')
# keywords as string in list
if (type(ld_json['keywords']) == list
and len(ld_json['keywords']) == 1
and ',' in ld_json['keywords'][0]):
ld_json['keywords'] = ld_json['keywords'][0].split(',')
# keywords as list
for kw in ld_json['keywords']:
if k := Keyword.objects.filter(name=kw).first():
keywords.append({'id': str(k.id), 'text': str(k).strip()})
else:
keywords.append({'id': random.randrange(1111111, 9999999, 1), 'text': kw.strip()})
ld_json['keywords'] = keywords
else:
ld_json['keywords'] = []
if 'recipeInstructions' in ld_json:
instructions = ''
# flatten instructions if they are in a list
if type(ld_json['recipeInstructions']) == list:
for i in ld_json['recipeInstructions']:
if type(i) == str:
instructions += i
else:
if 'text' in i:
instructions += i['text'] + '\n\n'
elif 'itemListElement' in i:
for ile in i['itemListElement']:
if type(ile) == str:
instructions += ile + '\n\n'
elif 'text' in ile:
instructions += ile['text'] + '\n\n'
else:
instructions += str(i)
ld_json['recipeInstructions'] = instructions
ld_json['recipeInstructions'] = re.sub(r'\n\s*\n', '\n\n', ld_json['recipeInstructions']) # noqa: E501
ld_json['recipeInstructions'] = re.sub(' +', ' ', ld_json['recipeInstructions']) # noqa: E501
ld_json['recipeInstructions'] = ld_json['recipeInstructions'].replace('<p>', '') # noqa: E501
ld_json['recipeInstructions'] = ld_json['recipeInstructions'].replace('</p>', '') # noqa: E501
else:
ld_json['recipeInstructions'] = ''
if url != '':
ld_json['recipeInstructions'] += '\n\n' + _('Imported from') + ' ' + url
if 'image' in ld_json:
# check if list of images is returned, take first if so
if (type(ld_json['image'])) == list:
if type(ld_json['image'][0]) == str:
ld_json['image'] = ld_json['image'][0]
elif 'url' in ld_json['image'][0]:
ld_json['image'] = ld_json['image'][0]['url']
# ignore relative image paths
if 'http' not in ld_json['image']:
ld_json['image'] = ''
if 'cookTime' in ld_json:
try:
if (type(ld_json['cookTime']) == list
and len(ld_json['cookTime']) > 0):
ld_json['cookTime'] = ld_json['cookTime'][0]
ld_json['cookTime'] = round(
parse_duration(
ld_json['cookTime']
).seconds / 60
)
except TypeError:
ld_json['cookTime'] = 0
else:
ld_json['cookTime'] = 0
if 'prepTime' in ld_json:
try:
if (type(ld_json['prepTime']) == list
and len(ld_json['prepTime']) > 0):
ld_json['prepTime'] = ld_json['prepTime'][0]
ld_json['prepTime'] = round(
parse_duration(
ld_json['prepTime']
).seconds / 60
)
except TypeError:
ld_json['prepTime'] = 0
else:
ld_json['prepTime'] = 0
except Exception:
ingredients.append(
{
'amount': 0,
'unit': {
'text': '',
'id': random.randrange(10000, 99999)
},
'ingredient': {
'text': x,
'id': random.randrange(10000, 99999)
},
'note': '',
'original': x
}
)
recipe_json['recipeIngredient'] = ingredients
except Exception:
recipe_json['recipeIngredient'] = ingredients
try:
if 'recipeYield' in ld_json:
if type(ld_json['recipeYield']) == str:
ld_json['servings'] = int(re.findall(r'\b\d+\b', ld_json['recipeYield'])[0])
except Exception as e:
print(e)
ld_json['servings'] = 1
recipe_json['recipeInstructions'] = parse_instructions(scrape.instructions())
except Exception:
recipe_json['recipeInstructions'] = ""
for key in list(ld_json):
if key not in [
'prepTime', 'cookTime', 'image', 'recipeInstructions',
'keywords', 'name', 'recipeIngredient', 'servings'
]:
ld_json.pop(key, None)
if scrape.url:
recipe_json['url'] = scrape.url
recipe_json['recipeInstructions'] += "\n\nImported from " + scrape.url
return recipe_json
return ld_json
def parse_name(name):
if type(name) == list:
try:
name = name[0]
except Exception:
name = 'ERROR'
return normalize_string(name)
def parse_ingredients(ingredients):
# some pages have comma separated ingredients in a single array entry
try:
if type(ingredients[0]) == dict:
return ingredients
except (KeyError, IndexError):
pass
if (len(ingredients) == 1 and type(ingredients) == list):
ingredients = ingredients[0].split(',')
elif type(ingredients) == str:
ingredients = ingredients.split(',')
for x in ingredients:
if '\n' in x:
ingredients.remove(x)
for i in x.split('\n'):
ingredients.insert(0, i)
ingredient_list = []
for x in ingredients:
if x.replace(' ', '') != '':
x = x.replace('&frac12;', "0.5").replace('&frac14;', "0.25").replace('&frac34;', "0.75")
try:
amount, unit, ingredient, note = parse_single_ingredient(x)
if ingredient:
ingredient_list.append(
{
'amount': amount,
'unit': {
'text': unit,
'id': random.randrange(10000, 99999)
},
'ingredient': {
'text': ingredient,
'id': random.randrange(10000, 99999)
},
'note': note,
'original': x
}
)
except Exception:
ingredient_list.append(
{
'amount': 0,
'unit': {
'text': '',
'id': random.randrange(10000, 99999)
},
'ingredient': {
'text': x,
'id': random.randrange(10000, 99999)
},
'note': '',
'original': x
}
)
ingredients = ingredient_list
else:
ingredients = []
return ingredients
def parse_description(description):
return normalize_string(description)
def parse_instructions(instructions):
instruction_text = ''
# flatten instructions if they are in a list
if type(instructions) == list:
for i in instructions:
if type(i) == str:
instruction_text += i
else:
if 'text' in i:
instruction_text += i['text'] + '\n\n'
elif 'itemListElement' in i:
for ile in i['itemListElement']:
if type(ile) == str:
instruction_text += ile + '\n\n'
elif 'text' in ile:
instruction_text += ile['text'] + '\n\n'
else:
instruction_text += str(i)
instructions = instruction_text
return normalize_string(instructions)
def parse_image(image):
# check if list of images is returned, take first if so
if not image:
return None
if type(image) == list:
for pic in image:
if (type(pic) == str) and (pic[:4] == 'http'):
image = pic
elif 'url' in pic:
image = pic['url']
elif type(image) == dict:
if 'url' in image:
image = image['url']
# ignore relative image paths
if image[:4] != 'http':
image = ''
return image
def parse_servings(servings):
if type(servings) == str:
try:
servings = int(re.search(r'\d+', servings).group())
except AttributeError:
servings = 1
elif type(servings) == list:
try:
servings = int(re.findall(r'\b\d+\b', servings[0])[0])
except KeyError:
servings = 1
return servings
def parse_cooktime(cooktime):
if type(cooktime) not in [int, float]:
try:
cooktime = float(re.search(r'\d+', cooktime).group())
except (ValueError, AttributeError):
try:
cooktime = round(iso_parse_duration(cooktime).seconds / 60)
except ISO8601Error:
try:
if (type(cooktime) == list and len(cooktime) > 0):
cooktime = cooktime[0]
cooktime = round(parse_duration(cooktime).seconds / 60)
except AttributeError:
cooktime = 0
return cooktime
def parse_preptime(preptime):
if type(preptime) not in [int, float]:
try:
preptime = float(re.search(r'\d+', preptime).group())
except ValueError:
try:
preptime = round(iso_parse_duration(preptime).seconds / 60)
except ISO8601Error:
try:
if (type(preptime) == list and len(preptime) > 0):
preptime = preptime[0]
preptime = round(parse_duration(preptime).seconds / 60)
except AttributeError:
preptime = 0
return preptime
def parse_keywords(keyword_json, space):
keywords = []
# keywords as list
for kw in keyword_json:
kw = normalize_string(kw)
if len(kw) != 0:
if k := Keyword.objects.filter(name=kw, space=space).first():
keywords.append({'id': str(k.id), 'text': str(k)})
else:
keywords.append({'id': random.randrange(1111111, 9999999, 1), 'text': kw})
return keywords
def listify_keywords(keyword_list):
# keywords as string
try:
if type(keyword_list[0]) == dict:
return keyword_list
except (KeyError, IndexError):
pass
if type(keyword_list) == str:
keyword_list = keyword_list.split(',')
# keywords as string in list
if (type(keyword_list) == list and len(keyword_list) == 1 and ',' in keyword_list[0]):
keyword_list = keyword_list[0].split(',')
return [x.strip() for x in keyword_list]
def normalize_string(string):
# Convert all named and numeric character references (e.g. &gt;, &#62;)
unescaped_string = unescape(string)
unescaped_string = re.sub('<[^<]+?>', '', unescaped_string)
unescaped_string = re.sub(' +', ' ', unescaped_string)
unescaped_string = re.sub('</p>', '\n', unescaped_string)
unescaped_string = re.sub(r'\n\s*\n', '\n\n', unescaped_string)
unescaped_string = unescaped_string.replace("\xa0", " ").replace("\t", " ").strip()
return unescaped_string

View 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)

View File

@ -0,0 +1,68 @@
import json
from recipe_scrapers._abstract import AbstractScraper
class CooksIllustrated(AbstractScraper):
@classmethod
def host(cls, site='cooksillustrated'):
return {
'cooksillustrated': f"{site}.com",
'americastestkitchen': f"{site}.com",
'cookscountry': f"{site}.com",
}.get(site)
def title(self):
return self.schema.title()
def image(self):
return self.schema.image()
def total_time(self):
if not self.recipe:
self.get_recipe()
return self.recipe['recipeTimeNote']
def yields(self):
if not self.recipe:
self.get_recipe()
return self.recipe['yields']
def ingredients(self):
if not self.recipe:
self.get_recipe()
ingredients = []
for group in self.recipe['ingredientGroups']:
ingredients += group['fields']['recipeIngredientItems']
return [
"{} {} {}{}".format(
i['fields']['qty'] or '',
i['fields']['measurement'] or '',
i['fields']['ingredient']['fields']['title'] or '',
i['fields']['postText'] or ''
)
for i in ingredients
]
def instructions(self):
if not self.recipe:
self.get_recipe()
if self.recipe.get('headnote', False):
i = ['Note: ' + self.recipe.get('headnote', '')]
else:
i = []
return "\n".join(
i
+ [self.recipe.get('whyThisWorks', '')]
+ [
instruction['fields']['content']
for instruction in self.recipe['instructions']
]
)
def nutrients(self):
raise NotImplementedError("This should be implemented.")
def get_recipe(self):
j = json.loads(self.soup.find(type='application/json').string)
name = list(j['props']['initialState']['content']['documents'])[0]
self.recipe = j['props']['initialState']['content']['documents'][name]

View File

@ -0,0 +1,43 @@
from bs4 import BeautifulSoup
from json import JSONDecodeError
from recipe_scrapers import SCRAPERS, get_host_name
from recipe_scrapers._factory import SchemaScraperFactory
from recipe_scrapers._schemaorg import SchemaOrg
from .cooksillustrated import CooksIllustrated
CUSTOM_SCRAPERS = {
CooksIllustrated.host(site="cooksillustrated"): CooksIllustrated,
CooksIllustrated.host(site="americastestkitchen"): CooksIllustrated,
CooksIllustrated.host(site="cookscountry"): CooksIllustrated,
}
SCRAPERS.update(CUSTOM_SCRAPERS)
def text_scraper(text, url=None):
domain = None
if url:
domain = get_host_name(url)
if domain in SCRAPERS:
scraper_class = SCRAPERS[domain]
else:
scraper_class = SchemaScraperFactory.SchemaScraper
class TextScraper(scraper_class):
def __init__(
self,
page_data,
url=None
):
self.wild_mode = False
# self.exception_handling = None # TODO add new method here, old one was deprecated
self.meta_http_equiv = False
self.soup = BeautifulSoup(page_data, "html.parser")
self.url = url
self.recipe = None
try:
self.schema = SchemaOrg(page_data)
except (JSONDecodeError, AttributeError):
pass
return TextScraper(text, url)

View File

@ -1,10 +1,10 @@
import bleach
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_urlize import UrlizeExtension
from jinja2 import Template, TemplateSyntaxError
from jinja2 import Template, TemplateSyntaxError, UndefinedError
from gettext import gettext as _
class IngredientObject(object):
amount = ""
@ -57,6 +57,8 @@ def render_instructions(step): # TODO deduplicate markdown cleanup code
template = Template(instructions)
instructions = template.render(ingredients=ingredients)
except TemplateSyntaxError:
pass
return _('Could not parse template code.') + ' Error: Template Syntax broken'
except UndefinedError:
return _('Could not parse template code.') + ' Error: Undefined Error'
return instructions

View File

@ -0,0 +1,59 @@
import json
import re
from io import BytesIO
from zipfile import ZipFile
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword
class Pepperplate(Integration):
def get_recipe_from_file(self, file):
ingredient_mode = False
direction_mode = False
ingredients = []
directions = []
for fl in file.readlines():
line = fl.decode("utf-8")
if 'Title:' in line:
title = line.replace('Title:', '').replace('"', '').strip()
if 'Description:' in line:
description = line.replace('Description:', '').strip()
if 'Original URL:' in line or 'Source:' in line or 'Yield:' in line or 'Total:' in line:
if len(line.strip().split(':')[1]) > 0:
directions.append(line.strip() + '\n')
if ingredient_mode:
if len(line) > 2 and 'Instructions:' not in line:
ingredients.append(line.strip())
if direction_mode:
if len(line) > 2:
directions.append(line.strip() + '\n')
if 'Ingredients:' in line:
ingredient_mode = True
if 'Instructions:' in line:
ingredient_mode = False
direction_mode = 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) + '\n\n'
)
for ingredient in ingredients:
if len(ingredient.strip()) > 0:
amount, unit, ingredient, note = parse(ingredient)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
))
recipe.steps.add(step)
return recipe
def get_file_from_recipe(self, recipe):
raise NotImplementedError('Method not implemented in storage integration')

View File

@ -0,0 +1,60 @@
import re
from django.utils.translation import gettext as _
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient
class ChefTap(Integration):
def import_file_name_filter(self, zip_info_object):
print("testing", zip_info_object.filename)
return re.match(r'^cheftap_export/([A-Za-z\d\w\s-])+.txt$', zip_info_object.filename) or re.match(r'^([A-Za-z\d\w\s-])+.txt$', zip_info_object.filename)
def get_recipe_from_file(self, file):
source_url = ''
ingredient_mode = 0
ingredients = []
directions = []
for i, fl in enumerate(file.readlines(), start=0):
line = fl.decode("utf-8")
if i == 0:
title = line.strip()
else:
if line.startswith('https:') or line.startswith('http:'):
source_url = line.strip()
else:
if ingredient_mode == 1 and len(line.strip()) == 0:
ingredient_mode = 2
if re.match(r'^([0-9])[^.](.)*$', line) and ingredient_mode < 2:
ingredient_mode = 1
ingredients.append(line.strip())
else:
directions.append(line.strip())
recipe = Recipe.objects.create(name=title, created_by=self.request.user, internal=True, space=self.request.space, )
step = Step.objects.create(instruction='\n'.join(directions))
if source_url != '':
step.instruction += '\n' + source_url
step.save()
for ingredient in ingredients:
if len(ingredient.strip()) > 0:
amount, unit, ingredient, note = parse(ingredient)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
))
recipe.steps.add(step)
return recipe
def get_file_from_recipe(self, recipe):
raise NotImplementedError('Method not implemented in storage integration')

View File

@ -3,7 +3,7 @@ import re
from io import BytesIO
from zipfile import ZipFile
from cookbook.helper.ingredient_parser import parse
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword
@ -47,10 +47,10 @@ class Chowdown(Integration):
if description_mode and len(line) > 3 and '---' not in 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(','):
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)
step = Step.objects.create(
@ -59,16 +59,16 @@ class Chowdown(Integration):
for ingredient in ingredients:
amount, unit, ingredient, note = parse(ingredient)
f, created = Food.objects.get_or_create(name=ingredient)
u, created = Unit.objects.get_or_create(name=unit)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
))
recipe.steps.add(step)
for f in self.files:
if '.zip' in f.name:
import_zip = ZipFile(f.file)
if '.zip' in f['name']:
import_zip = ZipFile(f['file'])
for z in import_zip.filelist:
if re.match(f'^images/{image}$', z.filename):
self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)))

View File

@ -0,0 +1,56 @@
import base64
import json
from io import BytesIO
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient
class Domestica(Integration):
def get_recipe_from_file(self, file):
recipe = Recipe.objects.create(
name=file['name'].strip(),
created_by=self.request.user, internal=True,
space=self.request.space)
if file['servings'] != '':
recipe.servings = file['servings']
if file['timeCook'] != '':
recipe.waiting_time = file['timeCook']
if file['timePrep'] != '':
recipe.working_time = file['timePrep']
recipe.save()
step = Step.objects.create(
instruction=file['directions']
)
if file['source'] != '':
step.instruction += '\n' + file['source']
for ingredient in file['ingredients'].split('\n'):
if len(ingredient.strip()) > 0:
amount, unit, ingredient, note = parse(ingredient)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
))
recipe.steps.add(step)
if file['image'] != '':
self.import_recipe_image(recipe, BytesIO(base64.b64decode(file['image'].replace('data:image/jpeg;base64,', ''))))
return recipe
def get_file_from_recipe(self, recipe):
raise NotImplementedError('Method not implemented in storage integration')
def split_recipe_file(self, file):
return json.loads(file.read().decode("utf-8"))

View File

@ -1,33 +1,38 @@
import datetime
import json
import uuid
from io import BytesIO, StringIO
from zipfile import ZipFile, BadZipFile
from django.contrib import messages
from django.core.files import File
from django.http import HttpResponseRedirect, HttpResponse
from django.urls import reverse
from django.http import HttpResponse
from django.utils.formats import date_format
from django.utils.translation import gettext as _
from cookbook.models import Keyword
from django_scopes import scope
from cookbook.forms import ImportExportBase
from cookbook.models import Keyword, Recipe
class Integration:
request = None
keyword = None
files = None
export_type = None
ignored_recipes = []
def __init__(self, request):
def __init__(self, request, export_type):
"""
Integration for importing and exporting recipes
:param request: request context of import session (used to link user to created objects)
"""
self.request = request
self.export_type = export_type
self.keyword = Keyword.objects.create(
name=f'Import {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")}',
icon='📥'
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")}. Type: {export_type}',
icon='📥',
space=request.space
)
def do_export(self, recipes):
@ -36,33 +41,44 @@ class Integration:
:param recipes: list of recipe objects
:return: HttpResponse with a ZIP file that is directly downloaded
"""
export_zip_stream = BytesIO()
export_zip_obj = ZipFile(export_zip_stream, 'w')
for r in recipes:
if r.internal:
recipe_zip_stream = BytesIO()
recipe_zip_obj = ZipFile(recipe_zip_stream, 'w')
# TODO this is temporary, find a better solution for different export formats when doing other exporters
if self.export_type != ImportExportBase.RECIPESAGE:
export_zip_stream = BytesIO()
export_zip_obj = ZipFile(export_zip_stream, 'w')
recipe_stream = StringIO()
filename, data = self.get_file_from_recipe(r)
recipe_stream.write(data)
recipe_zip_obj.writestr(filename, recipe_stream.getvalue())
recipe_stream.close()
for r in recipes:
if r.internal and r.space == self.request.space:
recipe_zip_stream = BytesIO()
recipe_zip_obj = ZipFile(recipe_zip_stream, 'w')
try:
recipe_zip_obj.write(r.image.path, 'image.png')
except ValueError:
pass
recipe_stream = StringIO()
filename, data = self.get_file_from_recipe(r)
recipe_stream.write(data)
recipe_zip_obj.writestr(filename, recipe_stream.getvalue())
recipe_stream.close()
recipe_zip_obj.close()
export_zip_obj.writestr(str(r.pk) + '.zip', recipe_zip_stream.getvalue())
try:
recipe_zip_obj.write(r.image.path, 'image.png')
except ValueError:
pass
export_zip_obj.close()
recipe_zip_obj.close()
export_zip_obj.writestr(str(r.pk) + '.zip', recipe_zip_stream.getvalue())
response = HttpResponse(export_zip_stream.getvalue(), content_type='application/force-download')
response['Content-Disposition'] = 'attachment; filename="export.zip"'
return response
export_zip_obj.close()
response = HttpResponse(export_zip_stream.getvalue(), content_type='application/force-download')
response['Content-Disposition'] = 'attachment; filename="export.zip"'
return response
else:
json_list = []
for r in recipes:
json_list.append(self.get_file_from_recipe(r))
response = HttpResponse(json.dumps(json_list), content_type='application/force-download')
response['Content-Disposition'] = 'attachment; filename="recipes.json"'
return response
def import_file_name_filter(self, zip_info_object):
"""
@ -74,29 +90,63 @@ class Integration:
"""
return True
def do_import(self, files):
def do_import(self, files, il, import_duplicates):
"""
Imports given files
:param import_duplicates: if true duplicates are imported as well
: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
"""
try:
self.files = files
for f in files:
if '.zip' in f.name:
import_zip = ZipFile(f.file)
for z in import_zip.filelist:
if self.import_file_name_filter(z):
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
recipe.keywords.add(self.keyword)
import_zip.close()
else:
recipe = self.get_recipe_from_file(f.file)
recipe.keywords.add(self.keyword)
except BadZipFile:
messages.add_message(self.request, messages.ERROR, _('Importer expected a .zip file. Did you choose the correct importer type for your data ?'))
with scope(space=self.request.space):
self.keyword.name = _('Import') + ' ' + str(il.pk)
self.keyword.save()
return HttpResponseRedirect(reverse('view_search') + '?keywords=' + str(self.keyword.pk))
try:
self.files = files
for f in files:
if '.zip' in f['name'] or '.paprikarecipes' in f['name']:
import_zip = ZipFile(f['file'])
for z in import_zip.filelist:
if self.import_file_name_filter(z):
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
recipe.keywords.add(self.keyword)
il.msg += f'{recipe.pk} - {recipe.name} \n'
self.handle_duplicates(recipe, import_duplicates)
import_zip.close()
elif '.json' in f['name'] or '.txt' in f['name']:
data_list = self.split_recipe_file(f['file'])
for d in data_list:
recipe = self.get_recipe_from_file(d)
recipe.keywords.add(self.keyword)
il.msg += f'{recipe.pk} - {recipe.name} \n'
self.handle_duplicates(recipe, import_duplicates)
else:
recipe = self.get_recipe_from_file(f['file'])
recipe.keywords.add(self.keyword)
il.msg += f'{recipe.pk} - {recipe.name} \n'
self.handle_duplicates(recipe, import_duplicates)
except BadZipFile:
il.msg += 'ERROR ' + _('Importer expected a .zip file. Did you choose the correct importer type for your data ?') + '\n'
if len(self.ignored_recipes) > 0:
il.msg += '\n' + _('The following recipes were ignored because they already existed:') + ' ' + ', '.join(self.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 handle_duplicates(self, recipe, import_duplicates):
"""
Checks if a recipe is already present, if so deletes it
:param recipe: Recipe object
:param import_duplicates: if duplicates should be imported
"""
if Recipe.objects.filter(space=self.request.space, name=recipe.name).count() > 1 and not import_duplicates:
recipe.delete()
self.ignored_recipes.append(recipe.name)
@staticmethod
def import_recipe_image(recipe, image_file):
@ -114,7 +164,15 @@ class Integration:
:param file: ByteIO or any file like object, depends on provider
:return: Recipe object
"""
raise NotImplementedError('Method not implemented in storage integration')
raise NotImplementedError('Method not implemented in integration')
def split_recipe_file(self, file):
"""
Takes a file that contains multiple recipes and splits it into a list of strings of various formats (e.g. json, text, ..)
:param file: ByteIO or any file like object, depends on provider
:return: list of strings
"""
raise NotImplementedError('Method not implemented in integration')
def get_file_from_recipe(self, recipe):
"""
@ -125,4 +183,4 @@ class Integration:
- name - file name in export
- data - string content for file to get created in export zip
"""
raise NotImplementedError('Method not implemented in storage integration')
raise NotImplementedError('Method not implemented in integration')

View File

@ -3,7 +3,7 @@ import re
from io import BytesIO
from zipfile import ZipFile
from cookbook.helper.ingredient_parser import parse
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient
@ -18,7 +18,7 @@ class Mealie(Integration):
recipe = Recipe.objects.create(
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 )
@ -32,16 +32,16 @@ class Mealie(Integration):
for ingredient in recipe_json['recipeIngredient']:
amount, unit, ingredient, note = parse(ingredient)
f, created = Food.objects.get_or_create(name=ingredient)
u, created = Unit.objects.get_or_create(name=unit)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
))
recipe.steps.add(step)
for f in self.files:
if '.zip' in f.name:
import_zip = ZipFile(f.file)
if '.zip' in f['name']:
import_zip = ZipFile(f['file'])
for z in import_zip.filelist:
if re.match(f'^images/{recipe_json["slug"]}.jpg$', z.filename):
self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)))

View File

@ -0,0 +1,83 @@
import json
import re
from io import BytesIO
from zipfile import ZipFile
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword
class MealMaster(Integration):
def get_recipe_from_file(self, file):
print('------------ getting recipe')
servings = 1
ingredients = []
directions = []
for line in file.replace('\r', '').split('\n'):
print('testing line')
if not line.startswith('MMMMM') and line.strip != '':
if 'Title:' in line:
title = line.replace('Title:', '').strip()
else:
if 'Categories:' in line:
tags = line.replace('Categories:', '').strip()
else:
if 'Yield:' in line:
servings_text = line.replace('Yield:', '').strip()
else:
if re.match('\s{2,}([0-9])+', line):
ingredients.append(line.strip())
else:
directions.append(line.strip())
try:
servings = re.findall('([0-9])+', servings_text)[0]
except Exception as e:
print('failed parsing servings ', e)
recipe = Recipe.objects.create(name=title, servings=servings, created_by=self.request.user, internal=True, space=self.request.space)
for k in tags.split(','):
keyword, created = Keyword.objects.get_or_create(name=k.strip(), space=self.request.space)
recipe.keywords.add(keyword)
step = Step.objects.create(
instruction='\n'.join(directions) + '\n\n'
)
for ingredient in ingredients:
if len(ingredient.strip()) > 0:
amount, unit, ingredient, note = parse(ingredient)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
))
recipe.steps.add(step)
return recipe
def get_file_from_recipe(self, recipe):
raise NotImplementedError('Method not implemented in storage integration')
def split_recipe_file(self, file):
recipe_list = []
current_recipe = ''
for fl in file.readlines():
line = fl.decode("ANSI")
if (line.startswith('MMMMM') or line.startswith('-----')) and 'meal-master' in line.lower():
if current_recipe != '':
recipe_list.append(current_recipe)
current_recipe = ''
else:
current_recipe = ''
else:
current_recipe += line + '\n'
if current_recipe != '':
recipe_list.append(current_recipe)
return recipe_list

View File

@ -3,7 +3,7 @@ import re
from io import BytesIO
from zipfile import ZipFile
from cookbook.helper.ingredient_parser import parse
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient
@ -19,7 +19,7 @@ class NextcloudCookbook(Integration):
recipe = Recipe.objects.create(
name=recipe_json['name'].strip(), description=recipe_json['description'].strip(),
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 keywords
@ -34,16 +34,16 @@ class NextcloudCookbook(Integration):
for ingredient in recipe_json['recipeIngredient']:
amount, unit, ingredient, note = parse(ingredient)
f, created = Food.objects.get_or_create(name=ingredient)
u, created = Unit.objects.get_or_create(name=unit)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
))
recipe.steps.add(step)
for f in self.files:
if '.zip' in f.name:
import_zip = ZipFile(f.file)
if '.zip' in f['name']:
import_zip = ZipFile(f['file'])
for z in import_zip.filelist:
if re.match(f'^Recipes/{recipe.name}/full.jpg$', z.filename):
self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)))

View File

@ -1,56 +1,83 @@
import base64
import gzip
import json
import re
from io import BytesIO
from zipfile import ZipFile
import microdata
from bs4 import BeautifulSoup
from cookbook.helper.recipe_url_import import find_recipe_json
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Ingredient, Unit
from cookbook.models import Recipe, Step, Ingredient, Keyword
from gettext import gettext as _
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):
raise NotImplementedError('Method not implemented in storage integration')
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"))
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(
instruction=recipe_json['recipeInstructions']
)
recipe = Recipe.objects.create(
name=recipe_json['name'].strip(), created_by=self.request.user, internal=True, space=self.request.space)
for ingredient in recipe_json['recipeIngredient']:
f, created = Food.objects.get_or_create(name=ingredient['ingredient']['text'])
u, created = Unit.objects.get_or_create(name=ingredient['unit']['text'])
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=ingredient['amount'], note=ingredient['note']
))
if 'description' in recipe_json:
recipe.description = recipe_json['description'].strip()
recipe.steps.add(step)
try:
if re.match(r'([0-9])+\s(.)*', recipe_json['servings'] ):
s = recipe_json['servings'].split(' ')
recipe.servings = s[0]
recipe.servings_text = s[1]
soup = BeautifulSoup(html_text, "html.parser")
image = soup.find('img')
image_name = image.attrs['src'].strip().replace('Images/', '')
if len(recipe_json['cook_time'].strip()) > 0:
recipe.waiting_time = re.findall(r'\d+', recipe_json['cook_time'])[0]
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)))
if len(recipe_json['prep_time'].strip()) > 0:
recipe.working_time = re.findall(r'\d+', recipe_json['prep_time'])[0]
except Exception:
pass
return recipe
recipe.save()
instructions = recipe_json['directions']
if recipe_json['notes'] and len(recipe_json['notes'].strip()) > 0:
instructions += '\n\n### ' + _('Notes') + ' \n' + recipe_json['notes']
if recipe_json['nutritional_info'] and len(recipe_json['nutritional_info'].strip()) > 0:
instructions += '\n\n### ' + _('Nutritional Information') + ' \n' + recipe_json['nutritional_info']
try:
if len(recipe_json['source'].strip()) > 0 or len(recipe_json['source_url'].strip()) > 0:
instructions += '\n\n### ' + _('Source') + ' \n' + recipe_json['source'].strip() + ' \n' + recipe_json['source_url'].strip()
except AttributeError:
pass
step = Step.objects.create(
instruction=instructions
)
if 'categories' in recipe_json:
for c in recipe_json['categories']:
keyword, created = Keyword.objects.get_or_create(name=c.strip(), space=self.request.space)
recipe.keywords.add(keyword)
try:
for ingredient in recipe_json['ingredients'].split('\n'):
if len(ingredient.strip()) > 0:
amount, unit, ingredient, note = parse(ingredient)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
))
except AttributeError:
pass
recipe.steps.add(step)
if recipe_json.get("photo_data", None):
self.import_recipe_image(recipe, BytesIO(base64.b64decode(recipe_json['photo_data'])))
return recipe

View File

@ -0,0 +1,93 @@
import base64
import json
from io import BytesIO
import requests
from rest_framework.renderers import JSONRenderer
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient
class RecipeSage(Integration):
def get_recipe_from_file(self, file):
recipe = Recipe.objects.create(
name=file['name'].strip(),
created_by=self.request.user, internal=True,
space=self.request.space)
try:
if file['recipeYield'] != '':
recipe.servings = int(file['recipeYield'])
if file['totalTime'] != '':
recipe.waiting_time = int(file['totalTime']) - int(file['timePrep'])
if file['prepTime'] != '':
recipe.working_time = int(file['timePrep'])
recipe.save()
except Exception as e:
print('failed to parse yield or time ', str(e))
ingredients_added = False
for s in file['recipeInstructions']:
step = Step.objects.create(
instruction=s['text']
)
if not ingredients_added:
ingredients_added = True
for ingredient in file['recipeIngredient']:
amount, unit, ingredient, note = parse(ingredient)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
))
recipe.steps.add(step)
if len(file['image']) > 0:
try:
response = requests.get(file['image'][0])
self.import_recipe_image(recipe, BytesIO(response.content))
except Exception as e:
print('failed to import image ', str(e))
return recipe
def get_file_from_recipe(self, recipe):
data = {
'@context': 'http://schema.org',
'@type': 'Recipe',
'creditText': '',
'isBasedOn': '',
'name': recipe.name,
'description': recipe.description,
'prepTime': str(recipe.working_time),
'totalTime': str(recipe.waiting_time + recipe.working_time),
'recipeYield': str(recipe.servings),
'image': [],
'recipeCategory': [],
'comment': [],
'recipeIngredient': [],
'recipeInstructions': [],
}
for s in recipe.steps.all():
if s.type != Step.TIME:
data['recipeInstructions'].append({
'@type': 'HowToStep',
'text': s.instruction
})
for i in s.ingredients.all():
data['recipeIngredient'].append(f'{float(i.amount)} {i.unit} {i.food}')
return data
def split_recipe_file(self, file):
return json.loads(file.read().decode("utf-8"))

View File

@ -0,0 +1,82 @@
import json
import re
from io import BytesIO
from zipfile import ZipFile
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword
class RezKonv(Integration):
def get_recipe_from_file(self, file):
ingredient_mode = False
direction_mode = False
ingredients = []
directions = []
for line in file.replace('\r', '').split('\n'):
if 'Titel:' in line:
title = line.replace('Titel:', '').strip()
if 'Kategorien:' in line:
tags = line.replace('Kategorien:', '').strip()
if ingredient_mode and ('quelle' in line.lower() or 'source' in line.lower()):
ingredient_mode = False
if ingredient_mode:
if line != '' and '===' not in line and 'Zubereitung' not in line:
ingredients.append(line.strip())
if direction_mode:
if line.strip() != '' and line.strip() != '=====':
directions.append(line.strip())
if 'Zutaten:' in line:
ingredient_mode = True
if 'Zubereitung:' in line:
ingredient_mode = False
direction_mode = True
recipe = Recipe.objects.create(name=title, created_by=self.request.user, internal=True, space=self.request.space)
for k in tags.split(','):
keyword, created = Keyword.objects.get_or_create(name=k.strip(), space=self.request.space)
recipe.keywords.add(keyword)
step = Step.objects.create(
instruction='\n'.join(directions) + '\n\n'
)
for ingredient in ingredients:
if len(ingredient.strip()) > 0:
amount, unit, ingredient, note = parse(ingredient)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
))
recipe.steps.add(step)
return recipe
def get_file_from_recipe(self, recipe):
raise NotImplementedError('Method not implemented in storage integration')
def split_recipe_file(self, file):
recipe_list = []
current_recipe = ''
for fl in file.readlines():
line = fl.decode("ANSI")
if line.startswith('=====') and 'rezkonv' in line.lower():
if current_recipe != '':
recipe_list.append(current_recipe)
current_recipe = ''
else:
current_recipe = ''
else:
current_recipe += line + '\n'
if current_recipe != '':
recipe_list.append(current_recipe)
return recipe_list

View File

@ -1,6 +1,6 @@
from django.utils.translation import gettext as _
from cookbook.helper.ingredient_parser import parse
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient
@ -41,14 +41,14 @@ class Safron(Integration):
ingredient_mode = False
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))
for ingredient in ingredients:
amount, unit, ingredient, note = parse(ingredient)
f, created = Food.objects.get_or_create(name=ingredient)
u, created = Unit.objects.get_or_create(name=unit)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
))

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,20 +1,22 @@
# Generated by Django 3.0.2 on 2020-01-30 09:59
from django.db import migrations
from django_scopes import scopes_disabled
def migrate_ingredient_units(apps, schema_editor):
Unit = apps.get_model('cookbook', 'Unit')
RecipeIngredients = apps.get_model('cookbook', 'RecipeIngredients')
with scopes_disabled():
Unit = apps.get_model('cookbook', 'Unit')
RecipeIngredients = apps.get_model('cookbook', 'RecipeIngredients')
for u in RecipeIngredients.objects.values('unit').distinct():
unit = Unit()
unit.name = u['unit']
unit.save()
for u in RecipeIngredients.objects.values('unit').distinct():
unit = Unit()
unit.name = u['unit']
unit.save()
for i in RecipeIngredients.objects.all():
i.unit_key = Unit.objects.get(name=i.unit)
i.save()
for i in RecipeIngredients.objects.all():
i.unit_key = Unit.objects.get(name=i.unit)
i.save()
class Migration(migrations.Migration):

View File

@ -1,19 +1,21 @@
# Generated by Django 3.0.2 on 2020-02-16 22:09
from django.db import migrations
from django_scopes import scopes_disabled
def migrate_ingredients(apps, schema_editor):
Ingredient = apps.get_model('cookbook', 'Ingredient')
RecipeIngredient = apps.get_model('cookbook', 'RecipeIngredient')
with scopes_disabled():
Ingredient = apps.get_model('cookbook', 'Ingredient')
RecipeIngredient = apps.get_model('cookbook', 'RecipeIngredient')
for u in RecipeIngredient.objects.values('name').distinct():
ingredient = Ingredient()
ingredient.name = u['name']
ingredient.save()
for u in RecipeIngredient.objects.values('name').distinct():
ingredient = Ingredient()
ingredient.name = u['name']
ingredient.save()
for i in RecipeIngredient.objects.all():
i.ingredient = Ingredient.objects.get(name=i.name)
i.save()
for i in RecipeIngredient.objects.all():
i.ingredient = Ingredient.objects.get(name=i.name)
i.save()
class Migration(migrations.Migration):

View File

@ -1,15 +1,17 @@
# Generated by Django 3.0.5 on 2020-04-26 14:14
from django.db import migrations
from django_scopes import scopes_disabled
def apply_migration(apps, schema_editor):
Group = apps.get_model('auth', 'Group')
Group.objects.bulk_create([
Group(name=u'guest'),
Group(name=u'user'),
Group(name=u'admin'),
])
with scopes_disabled():
Group = apps.get_model('auth', 'Group')
Group.objects.bulk_create([
Group(name=u'guest'),
Group(name=u'user'),
Group(name=u'admin'),
])
class Migration(migrations.Migration):

View File

@ -1,15 +1,17 @@
# Generated by Django 3.0.5 on 2020-04-27 16:00
from django.db import migrations
from django_scopes import scopes_disabled
def apply_migration(apps, schema_editor):
Group = apps.get_model('auth', 'Group')
User = apps.get_model('auth', 'User')
for u in User.objects.all():
if u.groups.count() < 1:
u.groups.add(Group.objects.get(name='admin'))
u.save()
with scopes_disabled():
Group = apps.get_model('auth', 'Group')
User = apps.get_model('auth', 'User')
for u in User.objects.all():
if u.groups.count() < 1:
u.groups.add(Group.objects.get(name='admin'))
u.save()
class Migration(migrations.Migration):

View File

@ -2,43 +2,45 @@
from django.db import migrations
from django.utils.translation import gettext as _
from django_scopes import scopes_disabled
def migrate_meal_types(apps, schema_editor):
MealPlan = apps.get_model('cookbook', 'MealPlan')
MealType = apps.get_model('cookbook', 'MealType')
with scopes_disabled():
MealPlan = apps.get_model('cookbook', 'MealPlan')
MealType = apps.get_model('cookbook', 'MealType')
breakfast = MealType.objects.create(
name=_('Breakfast'),
order=0,
)
breakfast = MealType.objects.create(
name=_('Breakfast'),
order=0,
)
lunch = MealType.objects.create(
name=_('Lunch'),
order=0,
)
lunch = MealType.objects.create(
name=_('Lunch'),
order=0,
)
dinner = MealType.objects.create(
name=_('Dinner'),
order=0,
)
dinner = MealType.objects.create(
name=_('Dinner'),
order=0,
)
other = MealType.objects.create(
name=_('Other'),
order=0,
)
other = MealType.objects.create(
name=_('Other'),
order=0,
)
for m in MealPlan.objects.all():
if m.meal == 'BREAKFAST':
m.meal_type = breakfast
if m.meal == 'LUNCH':
m.meal_type = lunch
if m.meal == 'DINNER':
m.meal_type = dinner
if m.meal == 'OTHER':
m.meal_type = other
for m in MealPlan.objects.all():
if m.meal == 'BREAKFAST':
m.meal_type = breakfast
if m.meal == 'LUNCH':
m.meal_type = lunch
if m.meal == 'DINNER':
m.meal_type = dinner
if m.meal == 'OTHER':
m.meal_type = other
m.save()
m.save()
class Migration(migrations.Migration):

View File

@ -2,22 +2,24 @@
from django.db import migrations
from django.db.models import Q
from django_scopes import scopes_disabled
def migrate_meal_types(apps, schema_editor):
MealPlan = apps.get_model('cookbook', 'MealPlan')
MealType = apps.get_model('cookbook', 'MealType')
User = apps.get_model('auth', 'User')
with scopes_disabled():
MealPlan = apps.get_model('cookbook', 'MealPlan')
MealType = apps.get_model('cookbook', 'MealType')
User = apps.get_model('auth', 'User')
for u in User.objects.all():
for t in MealType.objects.filter(created_by=None).all():
user_type = MealType.objects.create(
name=t.name,
created_by=u,
)
MealPlan.objects.filter(Q(created_by=u) and Q(meal_type=t)).update(meal_type=user_type)
for u in User.objects.all():
for t in MealType.objects.filter(created_by=None).all():
user_type = MealType.objects.create(
name=t.name,
created_by=u,
)
MealPlan.objects.filter(Q(created_by=u) and Q(meal_type=t)).update(meal_type=user_type)
MealType.objects.filter(created_by=None).delete()
MealType.objects.filter(created_by=None).delete()
class Migration(migrations.Migration):

View File

@ -3,11 +3,14 @@
from django.db import migrations, models
import uuid
from django_scopes import scopes_disabled
def invalidate_shares(apps, schema_editor):
ShareLink = apps.get_model('cookbook', 'ShareLink')
with scopes_disabled():
ShareLink = apps.get_model('cookbook', 'ShareLink')
ShareLink.objects.all().delete()
ShareLink.objects.all().delete()
class Migration(migrations.Migration):

View File

@ -1,16 +1,18 @@
# Generated by Django 3.0.7 on 2020-06-25 19:37
from django.db import migrations
from django_scopes import scopes_disabled
def migrate_ingredients(apps, schema_editor):
Recipe = apps.get_model('cookbook', 'Recipe')
Ingredient = apps.get_model('cookbook', 'Ingredient')
with scopes_disabled():
Recipe = apps.get_model('cookbook', 'Recipe')
Ingredient = apps.get_model('cookbook', 'Ingredient')
for r in Recipe.objects.all():
for i in Ingredient.objects.filter(recipe=r).all():
r.ingredients.add(i)
r.save()
for r in Recipe.objects.all():
for i in Ingredient.objects.filter(recipe=r).all():
r.ingredients.add(i)
r.save()
class Migration(migrations.Migration):

View File

@ -1,21 +1,23 @@
# Generated by Django 3.0.7 on 2020-06-25 20:19
from django.db import migrations, models
from django_scopes import scopes_disabled
def create_default_step(apps, schema_editor):
Recipe = apps.get_model('cookbook', 'Recipe')
Step = apps.get_model('cookbook', 'Step')
with scopes_disabled():
Recipe = apps.get_model('cookbook', 'Recipe')
Step = apps.get_model('cookbook', 'Step')
for r in Recipe.objects.filter(internal=True).all():
s = Step.objects.create(
instruction=r.instructions
)
for i in r.ingredients.all():
s.ingredients.add(i)
s.save()
r.steps.add(s)
r.save()
for r in Recipe.objects.filter(internal=True).all():
s = Step.objects.create(
instruction=r.instructions
)
for i in r.ingredients.all():
s.ingredients.add(i)
s.save()
r.steps.add(s)
r.save()
class Migration(migrations.Migration):

View File

@ -2,27 +2,29 @@
from django.db import migrations, models
import django.db.models.deletion
from django_scopes import scopes_disabled
def convert_old_specials(apps, schema_editor):
Ingredient = apps.get_model('cookbook', 'Ingredient')
Food = apps.get_model('cookbook', 'Food')
Unit = apps.get_model('cookbook', 'Unit')
with scopes_disabled():
Ingredient = apps.get_model('cookbook', 'Ingredient')
Food = apps.get_model('cookbook', 'Food')
Unit = apps.get_model('cookbook', 'Unit')
for i in Ingredient.objects.all():
if i.amount == 0:
i.no_amount = True
if i.unit.name == 'Special:Header':
i.header = True
i.unit = None
i.food = None
i.save()
for i in Ingredient.objects.all():
if i.amount == 0:
i.no_amount = True
if i.unit.name == 'Special:Header':
i.header = True
i.unit = None
i.food = None
i.save()
try:
Unit.objects.filter(name='Special:Header').delete()
Food.objects.filter(name='Header').delete()
except Exception:
pass
try:
Unit.objects.filter(name='Special:Header').delete()
Food.objects.filter(name='Header').delete()
except Exception:
pass
class Migration(migrations.Migration):

View 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,
),
]

View 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')},
),
]

View 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'),
),
]

View 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),
]

View 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',
),
]

View 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',
),
]

View 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),
),
]

View File

@ -0,0 +1,31 @@
# Generated by Django 3.1.7 on 2021-03-18 21:12
import cookbook.models
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('cookbook', '0114_importlog'),
]
operations = [
migrations.CreateModel(
name='TelegramBot',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('token', models.CharField(max_length=256)),
('name', models.CharField(blank=True, default='', max_length=128)),
('chat_id', models.CharField(blank=True, default='', max_length=128)),
('webhook_token', models.UUIDField(default=uuid.uuid4)),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('space', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space')),
],
bases=(models.Model, cookbook.models.PermissionModelMixin),
),
]

View File

@ -0,0 +1,41 @@
# Generated by Django 3.1.7 on 2021-03-18 23:12
from django.db import migrations
from django_scopes import scopes_disabled
def remove_empty_food_unit(apps, schema_editor):
with scopes_disabled():
Ingredient = apps.get_model('cookbook', 'Ingredient')
ShoppingListEntry = apps.get_model('cookbook', 'ShoppingListEntry')
Food = apps.get_model('cookbook', 'Food')
Unit = apps.get_model('cookbook', 'Unit')
for f in Food.objects.filter(name='').all():
for o in Ingredient.objects.filter(food=f):
o.food = None
o.save()
for o in ShoppingListEntry.objects.filter(food=f):
o.delete()
f.delete()
for u in Unit.objects.filter(name='').all():
for o in Ingredient.objects.filter(unit=u):
o.unit = None
o.save()
for o in ShoppingListEntry.objects.filter(unit=u):
o.unit = None
o.save()
u.delete()
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0115_telegrambot'),
]
operations = [
migrations.RunPython(remove_empty_food_unit),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-03-23 21:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0116_auto_20210319_0012'),
]
operations = [
migrations.AddField(
model_name='space',
name='max_recipes',
field=models.IntegerField(default=0),
),
]

View File

@ -0,0 +1,25 @@
# Generated by Django 3.1.7 on 2021-04-06 16:05
from django.db import migrations
from django_scopes import scopes_disabled
def migrate_no_group_superusers(apps, schema_editor):
with scopes_disabled():
User = apps.get_model('auth', 'User')
Groups = apps.get_model('auth', 'Group')
for u in User.objects.filter(is_superuser=True).all():
if u.groups.count() == 0:
u.groups.add(Groups.objects.get(name='admin'))
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0117_space_max_recipes'),
]
operations = [
migrations.RunPython(migrate_no_group_superusers),
]

View File

@ -0,0 +1,15 @@
# Generated by Django 3.2 on 2021-04-11 19:01
from django.contrib.postgres.operations import UnaccentExtension, TrigramExtension
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0118_auto_20210406_1805'),
]
operations = [
TrigramExtension(),
UnaccentExtension(),
]

View File

@ -0,0 +1,34 @@
# Generated by Django 3.1.7 on 2021-03-29 11:05
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', '0119_auto_20210411_2101'),
]
operations = [
migrations.AlterField(
model_name='userpreference',
name='use_fractions',
field=models.BooleanField(default=True),
),
migrations.CreateModel(
name='BookmarkletImport',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('html', models.TextField()),
('url', models.CharField(blank=True, max_length=256, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('space', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space')),
],
bases=(models.Model, cookbook.models.PermissionModelMixin),
),
]

View File

@ -9,7 +9,7 @@ from django.core.validators import MinLengthValidator
from django.db import models
from django.utils import timezone
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,
STICKY_NAV_PREF_DEFAULT)
@ -29,12 +29,45 @@ def get_model_name(model):
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):
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)
max_recipes = models.IntegerField(default=0)
def __str__(self):
return self.name
class UserPreference(models.Model):
class UserPreference(models.Model, PermissionModelMixin):
# Themes
BOOTSTRAP = 'BOOTSTRAP'
DARKLY = 'DARKLY'
@ -82,8 +115,9 @@ class UserPreference(models.Model):
# Search Style
SMALL = 'SMALL'
LARGE = 'LARGE'
NEW = 'NEW'
SEARCH_STYLE = ((SMALL, _('Small')), (LARGE, _('Large')),)
SEARCH_STYLE = ((SMALL, _('Small')), (LARGE, _('Large')), (NEW, _('New')))
user = AutoOneToOneField(User, on_delete=models.CASCADE, primary_key=True)
theme = models.CharField(choices=THEMES, max_length=128, default=FLATLY)
@ -107,11 +141,14 @@ class UserPreference(models.Model):
shopping_auto_sync = models.IntegerField(default=5)
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):
return str(self.user)
class Storage(models.Model):
class Storage(models.Model, PermissionModelMixin):
DROPBOX = 'DB'
NEXTCLOUD = 'NEXTCLOUD'
LOCAL = 'LOCAL'
@ -128,11 +165,14 @@ class Storage(models.Model):
path = models.CharField(blank=True, default='', max_length=256)
created_by = models.ForeignKey(User, on_delete=models.PROTECT)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
return self.name
class Sync(models.Model):
class Sync(models.Model, PermissionModelMixin):
storage = models.ForeignKey(Storage, on_delete=models.PROTECT)
path = models.CharField(max_length=512, default="")
active = models.BooleanField(default=True)
@ -140,92 +180,138 @@ class Sync(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
return self.path
class SupermarketCategory(models.Model):
name = models.CharField(unique=True, max_length=128, validators=[MinLengthValidator(1)])
class SupermarketCategory(models.Model, PermissionModelMixin):
name = models.CharField(max_length=128, validators=[MinLengthValidator(1)])
description = models.TextField(blank=True, null=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
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)
categories = models.ManyToManyField(SupermarketCategory, through='SupermarketCategoryRelation')
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
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')
category = models.ForeignKey(SupermarketCategory, on_delete=models.CASCADE, related_name='category_to_supermarket')
order = models.IntegerField(default=0)
objects = ScopedManager(space='supermarket__space')
@staticmethod
def get_space_key():
return 'supermarket', 'space'
class Meta:
ordering = ('order',)
class SyncLog(models.Model):
class SyncLog(models.Model, PermissionModelMixin):
sync = models.ForeignKey(Sync, on_delete=models.CASCADE)
status = models.CharField(max_length=32)
msg = models.TextField(default="")
created_at = models.DateTimeField(auto_now_add=True)
objects = ScopedManager(space='sync__space')
def __str__(self):
return f"{self.created_at}:{self.sync} - {self.status}"
class Keyword(models.Model):
name = models.CharField(max_length=64, unique=True)
class Keyword(models.Model, PermissionModelMixin):
name = models.CharField(max_length=64)
icon = models.CharField(max_length=16, blank=True, null=True)
description = models.TextField(default="", blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
if self.icon:
return f"{self.icon} {self.name}"
else:
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)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
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)
supermarket_category = models.ForeignKey(SupermarketCategory, null=True, blank=True, on_delete=models.SET_NULL)
ignore_shopping = models.BooleanField(default=False)
description = models.TextField(default='', blank=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
return self.name
class Meta:
unique_together = (('space', 'name'),)
class Ingredient(models.Model):
food = models.ForeignKey(
Food, on_delete=models.PROTECT, null=True, blank=True
)
unit = models.ForeignKey(
Unit, on_delete=models.PROTECT, null=True, blank=True
)
class Ingredient(models.Model, PermissionModelMixin):
food = models.ForeignKey(Food, 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)
note = models.CharField(max_length=256, null=True, blank=True)
is_header = models.BooleanField(default=False)
no_amount = models.BooleanField(default=False)
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):
return str(self.amount) + ' ' + str(self.unit) + ' ' + str(self.food)
@ -233,7 +319,7 @@ class Ingredient(models.Model):
ordering = ['order', 'pk']
class Step(models.Model):
class Step(models.Model, PermissionModelMixin):
TEXT = 'TEXT'
TIME = 'TIME'
@ -249,6 +335,15 @@ class Step(models.Model):
order = models.IntegerField(default=0)
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):
from cookbook.helper.template_helper import render_instructions
return render_instructions(self)
@ -257,7 +352,7 @@ class Step(models.Model):
ordering = ['order', 'pk']
class NutritionInformation(models.Model):
class NutritionInformation(models.Model, PermissionModelMixin):
fats = models.DecimalField(default=0, decimal_places=16, max_digits=32)
carbohydrates = models.DecimalField(
default=0, decimal_places=16, max_digits=32
@ -268,11 +363,20 @@ class NutritionInformation(models.Model):
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):
return 'Nutrition'
class Recipe(models.Model):
class Recipe(models.Model, PermissionModelMixin):
name = models.CharField(max_length=128)
description = models.CharField(max_length=512, blank=True, null=True)
servings = models.IntegerField(default=1)
@ -297,51 +401,71 @@ class Recipe(models.Model):
created_at = models.DateTimeField(auto_now_add=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):
return self.name
class Comment(models.Model):
class Comment(models.Model, PermissionModelMixin):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
text = models.TextField()
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
objects = ScopedManager(space='recipe__space')
@staticmethod
def get_space_key():
return 'recipe', 'space'
def get_space(self):
return self.recipe.space
def __str__(self):
return self.text
class RecipeImport(models.Model):
class RecipeImport(models.Model, PermissionModelMixin):
name = models.CharField(max_length=128)
storage = models.ForeignKey(Storage, on_delete=models.PROTECT)
file_uid = models.CharField(max_length=256, default="")
file_path = models.CharField(max_length=512, default="")
created_at = models.DateTimeField(auto_now_add=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
return self.name
class RecipeBook(models.Model):
class RecipeBook(models.Model, PermissionModelMixin):
name = models.CharField(max_length=128)
description = models.TextField(blank=True)
icon = models.CharField(max_length=16, blank=True, null=True)
shared = models.ManyToManyField(
User, blank=True, related_name='shared_with'
)
shared = models.ManyToManyField(User, blank=True, related_name='shared_with')
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
return self.name
class RecipeBookEntry(models.Model):
class RecipeBookEntry(models.Model, PermissionModelMixin):
recipe = models.ForeignKey(Recipe, 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):
return self.recipe.name
@ -355,29 +479,31 @@ class RecipeBookEntry(models.Model):
unique_together = (('recipe', 'book'),)
class MealType(models.Model):
class MealType(models.Model, PermissionModelMixin):
name = models.CharField(max_length=128)
order = models.IntegerField(default=0)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
return self.name
class MealPlan(models.Model):
recipe = models.ForeignKey(
Recipe, on_delete=models.CASCADE, blank=True, null=True
)
class MealPlan(models.Model, PermissionModelMixin):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, blank=True, null=True)
servings = models.DecimalField(default=1, max_digits=8, decimal_places=4)
title = models.CharField(max_length=64, blank=True, default='')
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
shared = models.ManyToManyField(
User, blank=True, related_name='plan_share'
)
shared = models.ManyToManyField(User, blank=True, related_name='plan_share')
meal_type = models.ForeignKey(MealType, on_delete=models.CASCADE)
note = models.TextField(blank=True)
date = models.DateField()
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def get_label(self):
if self.title:
return self.title
@ -390,12 +516,19 @@ class MealPlan(models.Model):
return f'{self.get_label()} - {self.date} - {self.meal_type.name}'
class ShoppingListRecipe(models.Model):
recipe = models.ForeignKey(
Recipe, on_delete=models.CASCADE, null=True, blank=True
)
class ShoppingListRecipe(models.Model, PermissionModelMixin):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, null=True, blank=True)
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):
return f'Shopping list recipe {self.id} - {self.recipe}'
@ -406,7 +539,7 @@ class ShoppingListRecipe(models.Model):
return None
class ShoppingListEntry(models.Model):
class ShoppingListEntry(models.Model, PermissionModelMixin):
list_recipe = models.ForeignKey(ShoppingListRecipe, on_delete=models.CASCADE, null=True, blank=True)
food = models.ForeignKey(Food, on_delete=models.CASCADE)
unit = models.ForeignKey(Unit, on_delete=models.CASCADE, null=True, blank=True)
@ -414,9 +547,21 @@ class ShoppingListEntry(models.Model):
order = models.IntegerField(default=0)
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):
return f'Shopping list entry {self.id}'
def get_shared(self):
return self.shoppinglist_set.first().shared.all()
def get_owner(self):
try:
return self.shoppinglist_set.first().created_by
@ -424,7 +569,7 @@ class ShoppingListEntry(models.Model):
return None
class ShoppingList(models.Model):
class ShoppingList(models.Model, PermissionModelMixin):
uuid = models.UUIDField(default=uuid.uuid4)
note = models.TextField(blank=True, null=True)
recipes = models.ManyToManyField(ShoppingListRecipe, blank=True)
@ -435,16 +580,22 @@ class ShoppingList(models.Model):
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
return f'Shopping list {self.id}'
class ShareLink(models.Model):
class ShareLink(models.Model, PermissionModelMixin):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
uuid = models.UUIDField(default=uuid.uuid4)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
return f'{self.recipe} - {self.uuid}'
@ -453,7 +604,7 @@ def default_valid_until():
return date.today() + timedelta(days=14)
class InviteLink(models.Model):
class InviteLink(models.Model, PermissionModelMixin):
uuid = models.UUIDField(default=uuid.uuid4)
username = models.CharField(blank=True, max_length=64)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
@ -464,25 +615,73 @@ class InviteLink(models.Model):
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
return f'{self.uuid}'
class CookLog(models.Model):
class TelegramBot(models.Model, PermissionModelMixin):
token = models.CharField(max_length=256)
name = models.CharField(max_length=128, default='', blank=True)
chat_id = models.CharField(max_length=128, default='', blank=True)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
webhook_token = models.UUIDField(default=uuid.uuid4)
objects = ScopedManager(space='space')
space = models.ForeignKey(Space, on_delete=models.CASCADE)
def __str__(self):
return f"{self.name}"
class CookLog(models.Model, PermissionModelMixin):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(default=timezone.now)
rating = models.IntegerField(null=True)
servings = models.IntegerField(default=0)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
return self.recipe.name
class ViewLog(models.Model):
class ViewLog(models.Model, PermissionModelMixin):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
return 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}"
class BookmarkletImport(models.Model, PermissionModelMixin):
html = models.TextField()
url = models.CharField(max_length=256, null=True, blank=True)
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)

View File

@ -35,14 +35,14 @@ class Dropbox(Provider):
# TODO check if has_more is set and import that as well
for recipe in recipes['entries']:
path = recipe['path_lower']
if not Recipe.objects.filter(file_path__iexact=path).exists() \
and not RecipeImport.objects.filter(file_path=path).exists(): # noqa: E501
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():
name = os.path.splitext(recipe['name'])[0]
new_recipe = RecipeImport(
name=name,
file_path=path,
storage=monitor.storage,
file_uid=recipe['id']
file_uid=recipe['id'],
space=monitor.space,
)
new_recipe.save()
import_count += 1
@ -50,7 +50,7 @@ class Dropbox(Provider):
log_entry = SyncLog(
status='SUCCESS',
msg='Imported ' + str(import_count) + ' recipes',
sync=monitor
sync=monitor,
)
log_entry.save()
@ -104,9 +104,7 @@ class Dropbox(Provider):
recipe.link = Dropbox.get_share_link(recipe)
recipe.save()
response = requests.get(
recipe.link.replace('www.dropbox.', 'dl.dropboxusercontent.')
)
response = requests.get(recipe.link.replace('www.dropbox.', 'dl.dropboxusercontent.'))
return io.BytesIO(response.content)

View File

@ -18,13 +18,13 @@ class Local(Provider):
import_count = 0
for file in files:
path = monitor.path + '/' + file
if not Recipe.objects.filter(file_path__iexact=path).exists() \
and not RecipeImport.objects.filter(file_path=path).exists(): # noqa: E501
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():
name = os.path.splitext(file)[0]
new_recipe = RecipeImport(
name=name,
file_path=path,
storage=monitor.storage
storage=monitor.storage,
space=monitor.space,
)
new_recipe.save()
import_count += 1
@ -32,7 +32,7 @@ class Local(Provider):
log_entry = SyncLog(
status='SUCCESS',
msg='Imported ' + str(import_count) + ' recipes',
sync=monitor
sync=monitor,
)
log_entry.save()

View File

@ -34,13 +34,13 @@ class Nextcloud(Provider):
import_count = 0
for file in files:
path = monitor.path + '/' + file
if not Recipe.objects.filter(file_path__iexact=path).exists() \
and not RecipeImport.objects.filter(file_path=path).exists(): # noqa: E501
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():
name = os.path.splitext(file)[0]
new_recipe = RecipeImport(
name=name,
file_path=path,
storage=monitor.storage
storage=monitor.storage,
space=monitor.space,
)
new_recipe.save()
import_count += 1
@ -48,7 +48,7 @@ class Nextcloud(Provider):
log_entry = SyncLog(
status='SUCCESS',
msg='Imported ' + str(import_count) + ' recipes',
sync=monitor
sync=monitor,
)
log_entry.save()
@ -68,14 +68,7 @@ class Nextcloud(Provider):
data = {'path': recipe.file_path, 'shareType': 3}
r = requests.post(
url,
headers=headers,
auth=HTTPBasicAuth(
recipe.storage.username, recipe.storage.password
),
data=data
)
r = requests.post(url, headers=headers, auth=HTTPBasicAuth(recipe.storage.username, recipe.storage.password), data=data)
response_json = r.json()

View File

@ -1,17 +1,21 @@
from decimal import Decimal
from django.contrib.auth.models import User
from django.db.models import QuerySet
from drf_writable_nested import (UniqueFieldsMixin,
WritableNestedModelSerializer)
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,
MealPlan, MealType, NutritionInformation, Recipe,
RecipeBook, RecipeBookEntry, RecipeImport,
ShareLink, ShoppingList, ShoppingListEntry,
ShoppingListRecipe, Step, Storage, Sync, SyncLog,
Unit, UserPreference, ViewLog, SupermarketCategory, Supermarket, SupermarketCategoryRelation)
Unit, UserPreference, ViewLog, SupermarketCategory, Supermarket,
SupermarketCategoryRelation, ImportLog, BookmarkletImport)
from cookbook.templatetags.custom_tags import markdown
@ -39,6 +43,38 @@ class CustomDecimalField(serializers.Field):
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):
username = serializers.SerializerMethodField('get_user_label')
@ -46,11 +82,18 @@ class UserNameSerializer(WritableNestedModelSerializer):
return obj.get_user_name()
class Meta:
list_serializer_class = SpaceFilterSerializer
model = User
fields = ('id', 'username')
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:
model = UserPreference
fields = (
@ -58,10 +101,14 @@ class UserPreferenceSerializer(serializers.ModelSerializer):
'search_style', 'show_recent', 'plan_share', 'ingredient_decimals',
'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:
model = Storage
fields = (
@ -69,13 +116,15 @@ class StorageSerializer(serializers.ModelSerializer):
'token', 'created_by'
)
read_only_fields = ('created_by',)
extra_kwargs = {
'password': {'write_only': True},
'token': {'write_only': True},
}
class SyncSerializer(serializers.ModelSerializer):
class SyncSerializer(SpacedModelSerializer):
class Meta:
model = Sync
fields = (
@ -84,7 +133,7 @@ class SyncSerializer(serializers.ModelSerializer):
)
class SyncLogSerializer(serializers.ModelSerializer):
class SyncLogSerializer(SpacedModelSerializer):
class Meta:
model = SyncLog
fields = ('id', 'sync', 'status', 'msg', 'created_at')
@ -97,6 +146,7 @@ class KeywordLabelSerializer(serializers.ModelSerializer):
return str(obj)
class Meta:
list_serializer_class = SpaceFilterSerializer
model = Keyword
fields = (
'id', 'label',
@ -111,17 +161,13 @@ class KeywordSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
return str(obj)
def create(self, validated_data):
# since multi select tags dont have id's
# duplicate names might be routed to create
obj, created = Keyword.objects.get_or_create(name=validated_data['name'])
obj, created = Keyword.objects.get_or_create(name=validated_data['name'].strip(), space=self.context['request'].space)
return obj
class Meta:
list_serializer_class = SpaceFilterSerializer
model = Keyword
fields = (
'id', 'name', 'icon', 'label', 'description',
'created_at', 'updated_at'
)
fields = ('id', 'name', 'icon', 'label', 'description', 'created_at', 'updated_at')
read_only_fields = ('id',)
@ -129,9 +175,7 @@ class KeywordSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
class UnitSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
def create(self, validated_data):
# since multi select tags dont have id's
# duplicate names might be routed to create
obj, created = Unit.objects.get_or_create(name=validated_data['name'])
obj, created = Unit.objects.get_or_create(name=validated_data['name'].strip(), space=self.context['request'].space)
return obj
class Meta:
@ -143,9 +187,7 @@ class UnitSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
class SupermarketCategorySerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
def create(self, validated_data):
# since multi select tags dont have id's
# duplicate names might be routed to create
obj, created = SupermarketCategory.objects.get_or_create(name=validated_data['name'])
obj, created = SupermarketCategory.objects.get_or_create(name=validated_data['name'], space=self.context['request'].space)
return obj
def update(self, instance, validated_data):
@ -156,7 +198,7 @@ class SupermarketCategorySerializer(UniqueFieldsMixin, WritableNestedModelSerial
fields = ('id', 'name')
class SupermarketCategoryRelationSerializer(serializers.ModelSerializer):
class SupermarketCategoryRelationSerializer(SpacedModelSerializer):
category = SupermarketCategorySerializer()
class Meta:
@ -164,7 +206,7 @@ class SupermarketCategoryRelationSerializer(serializers.ModelSerializer):
fields = ('id', 'category', 'supermarket', 'order')
class SupermarketSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
class SupermarketSerializer(UniqueFieldsMixin, SpacedModelSerializer):
category_to_supermarket = SupermarketCategoryRelationSerializer(many=True, read_only=True)
class Meta:
@ -176,9 +218,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
supermarket_category = SupermarketCategorySerializer(allow_null=True, required=False)
def create(self, validated_data):
# since multi select tags dont have id's
# duplicate names might be routed to create
obj, created = Food.objects.get_or_create(name=validated_data['name'])
obj, created = Food.objects.get_or_create(name=validated_data['name'].strip(), space=self.context['request'].space)
return obj
def update(self, instance, validated_data):
@ -230,6 +270,12 @@ class NutritionInformationSerializer(serializers.ModelSerializer):
class RecipeOverviewSerializer(WritableNestedModelSerializer):
keywords = KeywordLabelSerializer(many=True)
def create(self, validated_data):
pass
def update(self, instance, validated_data):
return instance
class Meta:
model = Recipe
fields = (
@ -256,6 +302,7 @@ class RecipeSerializer(WritableNestedModelSerializer):
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)
@ -265,7 +312,7 @@ class RecipeImageSerializer(WritableNestedModelSerializer):
fields = ['image', ]
class RecipeImportSerializer(serializers.ModelSerializer):
class RecipeImportSerializer(SpacedModelSerializer):
class Meta:
model = RecipeImport
fields = '__all__'
@ -277,26 +324,33 @@ class CommentSerializer(serializers.ModelSerializer):
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:
model = RecipeBook
fields = '__all__'
read_only_fields = ['id', 'created_by']
fields = ('id', 'name', 'description', 'icon', 'shared', 'created_by')
read_only_fields = ('created_by',)
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:
model = RecipeBookEntry
fields = '__all__'
fields = ('id', 'book', 'recipe',)
class MealTypeSerializer(serializers.ModelSerializer):
class Meta:
model = MealType
fields = '__all__'
class MealPlanSerializer(serializers.ModelSerializer):
class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
recipe = RecipeOverviewSerializer(required=False)
recipe_name = serializers.ReadOnlyField(source='recipe.name')
meal_type_name = serializers.ReadOnlyField(source='meal_type.name')
note_markdown = serializers.SerializerMethodField('get_note_markdown')
@ -305,6 +359,10 @@ class MealPlanSerializer(serializers.ModelSerializer):
def get_note_markdown(self, obj):
return markdown(obj.note)
def create(self, validated_data):
validated_data['created_by'] = self.context['request'].user
return super().create(validated_data)
class Meta:
model = MealPlan
fields = (
@ -312,6 +370,7 @@ class MealPlanSerializer(serializers.ModelSerializer):
'date', 'meal_type', 'created_by', 'shared', 'recipe_name',
'meal_type_name'
)
read_only_fields = ('created_by',)
class ShoppingListRecipeSerializer(serializers.ModelSerializer):
@ -348,13 +407,18 @@ class ShoppingListSerializer(WritableNestedModelSerializer):
shared = UserNameSerializer(many=True)
supermarket = SupermarketSerializer(allow_null=True)
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
validated_data['created_by'] = self.context['request'].user
return super().create(validated_data)
class Meta:
model = ShoppingList
fields = (
'id', 'uuid', 'note', 'recipes', 'entries',
'shared', 'finished', 'supermarket', 'created_by', 'created_at'
)
read_only_fields = ('id',)
read_only_fields = ('id', 'created_by',)
class ShoppingListAutoSyncSerializer(WritableNestedModelSerializer):
@ -366,27 +430,63 @@ class ShoppingListAutoSyncSerializer(WritableNestedModelSerializer):
read_only_fields = ('id',)
class ShareLinkSerializer(serializers.ModelSerializer):
class ShareLinkSerializer(SpacedModelSerializer):
class Meta:
model = ShareLink
fields = '__all__'
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['space'] = self.context['request'].space
return super().create(validated_data)
class Meta:
model = CookLog
fields = '__all__'
fields = ('id', 'recipe', 'servings', 'rating', 'created_by', 'created_at')
read_only_fields = ('id', 'created_by')
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:
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',)
# CORS, REST and Scopes aren't currently working
# Scopes are evaluating before REST has authenticated the user assiging a None space
# I've made the change below to fix the bookmarklet, other serializers likely need a similar/better fix
class BookmarkletImportSerializer(serializers.ModelSerializer):
def create(self, validated_data):
validated_data['created_by'] = self.context['request'].user
validated_data['space'] = self.context['request'].user.userpreference.space
return super().create(validated_data)
class Meta:
model = BookmarkletImport
fields = ('id', 'url', 'html', 'created_by', 'created_at')
read_only_fields = ('created_by', 'space')
# Export/Import Serializers
@ -455,4 +555,5 @@ class RecipeExportSerializer(WritableNestedModelSerializer):
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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Some files were not shown because too many files have changed in this diff Show More