Merge remote-tracking branch 'upstream/develop' into database-url
This commit is contained in:
commit
7650edfdc8
@ -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
|
18
.github/ISSUE_TEMPLATE/help-request.md
vendored
18
.github/ISSUE_TEMPLATE/help-request.md
vendored
@ -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
22
.github/ISSUE_TEMPLATE/url_import.md
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
---
|
||||
name: Website Import
|
||||
about: Anything related to website imports
|
||||
title: ''
|
||||
labels: enhancement, url_import
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
### Version
|
||||
Please provide your current version (can be found on the system page since v0.8.4)
|
||||
Version:
|
||||
|
||||
### Information
|
||||
Exact URL you are trying to import from:
|
||||
|
||||
When did the issue happen: When pressing the search button with the url / when importing after the page has loaded
|
||||
|
||||
Response/Message shown
|
||||
```
|
||||
Message
|
||||
```
|
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@ -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
|
||||
|
64
.github/workflows/codeql-analysis.yml
vendored
64
.github/workflows/codeql-analysis.yml
vendored
@ -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
1
.gitignore
vendored
@ -78,3 +78,4 @@ postgresql/
|
||||
|
||||
/docker-compose.override.yml
|
||||
vue/node_modules
|
||||
.vscode/
|
||||
|
@ -7,6 +7,7 @@
|
||||
<w>gunicorn</w>
|
||||
<w>ical</w>
|
||||
<w>mealie</w>
|
||||
<w>pepperplate</w>
|
||||
<w>safron</w>
|
||||
<w>traefik</w>
|
||||
</words>
|
||||
|
@ -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>
|
18
Dockerfile
18
Dockerfile
@ -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"]
|
@ -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)
|
||||
|
@ -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']
|
||||
|
@ -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):
|
||||
|
8
cookbook/helper/CustomTestRunner.py
Normal file
8
cookbook/helper/CustomTestRunner.py
Normal file
@ -0,0 +1,8 @@
|
||||
from django.test.runner import DiscoverRunner
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
|
||||
class CustomTestRunner(DiscoverRunner):
|
||||
def run_tests(self, *args, **kwargs):
|
||||
with scopes_disabled():
|
||||
return super().run_tests(*args, **kwargs)
|
@ -10,7 +10,7 @@ class BaseAutocomplete(autocomplete.Select2QuerySetView):
|
||||
if not self.request.user.is_authenticated:
|
||||
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)
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
|
193
cookbook/helper/recipe_html_import.py
Normal file
193
cookbook/helper/recipe_html_import.py
Normal 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
|
66
cookbook/helper/recipe_search.py
Normal file
66
cookbook/helper/recipe_search.py
Normal 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
|
@ -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('½', "0.5").replace('¼', "0.25").replace('¾', "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. >, >)
|
||||
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
|
||||
|
33
cookbook/helper/scope_middleware.py
Normal file
33
cookbook/helper/scope_middleware.py
Normal file
@ -0,0 +1,33 @@
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
from django_scopes import scope, scopes_disabled
|
||||
|
||||
from cookbook.views import views
|
||||
|
||||
|
||||
class ScopeMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
if request.user.is_authenticated:
|
||||
|
||||
if request.path.startswith('/admin/'):
|
||||
with scopes_disabled():
|
||||
return self.get_response(request)
|
||||
|
||||
with scopes_disabled():
|
||||
if request.user.userpreference.space is None and not reverse('account_logout') in request.path:
|
||||
return views.no_space(request)
|
||||
|
||||
if request.user.groups.count() == 0 and not reverse('account_logout') in request.path:
|
||||
return views.no_groups(request)
|
||||
|
||||
request.space = request.user.userpreference.space
|
||||
# with scopes_disabled():
|
||||
with scope(space=request.space):
|
||||
return self.get_response(request)
|
||||
else:
|
||||
with scopes_disabled():
|
||||
request.space = None
|
||||
return self.get_response(request)
|
68
cookbook/helper/scrapers/cooksillustrated.py
Normal file
68
cookbook/helper/scrapers/cooksillustrated.py
Normal 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]
|
43
cookbook/helper/scrapers/scrapers.py
Normal file
43
cookbook/helper/scrapers/scrapers.py
Normal 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)
|
@ -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
|
||||
|
59
cookbook/integration/Pepperplate.py
Normal file
59
cookbook/integration/Pepperplate.py
Normal 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')
|
60
cookbook/integration/cheftap.py
Normal file
60
cookbook/integration/cheftap.py
Normal 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')
|
@ -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)))
|
||||
|
56
cookbook/integration/domestica.py
Normal file
56
cookbook/integration/domestica.py
Normal 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"))
|
@ -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')
|
||||
|
@ -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)))
|
||||
|
83
cookbook/integration/mealmaster.py
Normal file
83
cookbook/integration/mealmaster.py
Normal 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
|
@ -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)))
|
||||
|
@ -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
|
||||
|
93
cookbook/integration/recipesage.py
Normal file
93
cookbook/integration/recipesage.py
Normal 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"))
|
82
cookbook/integration/rezkonv.py
Normal file
82
cookbook/integration/rezkonv.py
Normal 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
|
@ -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
|
||||
))
|
||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
cookbook/locale/hy/LC_MESSAGES/django.mo
Normal file
BIN
cookbook/locale/hy/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
1881
cookbook/locale/hy/LC_MESSAGES/django.po
Normal file
1881
cookbook/locale/hy/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
1832
cookbook/locale/nb_NO/LC_MESSAGES/django.po
Normal file
1832
cookbook/locale/nb_NO/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
cookbook/locale/pl/LC_MESSAGES/django.mo
Normal file
BIN
cookbook/locale/pl/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
1797
cookbook/locale/pl/LC_MESSAGES/django.po
Normal file
1797
cookbook/locale/pl/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
1853
cookbook/locale/sv/LC_MESSAGES/django.po
Normal file
1853
cookbook/locale/sv/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
cookbook/locale/vi/LC_MESSAGES/django.mo
Normal file
BIN
cookbook/locale/vi/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
1787
cookbook/locale/vi/LC_MESSAGES/django.po
Normal file
1787
cookbook/locale/vi/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
146
cookbook/migrations/0108_auto_20210219_1410.py
Normal file
146
cookbook/migrations/0108_auto_20210219_1410.py
Normal file
@ -0,0 +1,146 @@
|
||||
# Generated by Django 3.1.6 on 2021-02-19 13:10
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0107_auto_20210128_1535'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='cooklog',
|
||||
name='space',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='food',
|
||||
name='space',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invitelink',
|
||||
name='space',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='keyword',
|
||||
name='space',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='mealplan',
|
||||
name='space',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='mealtype',
|
||||
name='space',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='recipe',
|
||||
name='space',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='recipebook',
|
||||
name='space',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='recipebookentry',
|
||||
name='space',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='recipeimport',
|
||||
name='space',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sharelink',
|
||||
name='space',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='shoppinglist',
|
||||
name='space',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='shoppinglistentry',
|
||||
name='space',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='shoppinglistrecipe',
|
||||
name='space',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='storage',
|
||||
name='space',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='supermarket',
|
||||
name='space',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='supermarketcategory',
|
||||
name='space',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sync',
|
||||
name='space',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='synclog',
|
||||
name='space',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='unit',
|
||||
name='space',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userpreference',
|
||||
name='space',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='viewlog',
|
||||
name='space',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
63
cookbook/migrations/0109_auto_20210221_1204.py
Normal file
63
cookbook/migrations/0109_auto_20210221_1204.py
Normal file
@ -0,0 +1,63 @@
|
||||
# Generated by Django 3.1.6 on 2021-02-21 11:04
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0108_auto_20210219_1410'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='recipebookentry',
|
||||
name='space',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='food',
|
||||
name='name',
|
||||
field=models.CharField(max_length=128, validators=[django.core.validators.MinLengthValidator(1)]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='keyword',
|
||||
name='name',
|
||||
field=models.CharField(max_length=64),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='supermarket',
|
||||
name='name',
|
||||
field=models.CharField(max_length=128, validators=[django.core.validators.MinLengthValidator(1)]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='supermarketcategory',
|
||||
name='name',
|
||||
field=models.CharField(max_length=128, validators=[django.core.validators.MinLengthValidator(1)]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='unit',
|
||||
name='name',
|
||||
field=models.CharField(max_length=128, validators=[django.core.validators.MinLengthValidator(1)]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='food',
|
||||
unique_together={('space', 'name')},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='keyword',
|
||||
unique_together={('space', 'name')},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='supermarket',
|
||||
unique_together={('space', 'name')},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='supermarketcategory',
|
||||
unique_together={('space', 'name')},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='unit',
|
||||
unique_together={('space', 'name')},
|
||||
),
|
||||
]
|
19
cookbook/migrations/0110_auto_20210221_1406.py
Normal file
19
cookbook/migrations/0110_auto_20210221_1406.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.1.6 on 2021-02-21 13:06
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0109_auto_20210221_1204'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='userpreference',
|
||||
name='space',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
|
||||
),
|
||||
]
|
32
cookbook/migrations/0111_space_created_by.py
Normal file
32
cookbook/migrations/0111_space_created_by.py
Normal file
@ -0,0 +1,32 @@
|
||||
# Generated by Django 3.1.6 on 2021-02-21 13:19
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
|
||||
def set_default_owner(apps, schema_editor):
|
||||
Space = apps.get_model('cookbook', 'Space')
|
||||
User = apps.get_model('auth', 'user')
|
||||
|
||||
with scopes_disabled():
|
||||
for x in Space.objects.all():
|
||||
x.created_by = User.objects.filter(is_superuser=True).first()
|
||||
x.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('cookbook', '0110_auto_20210221_1406'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='space',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.RunPython(set_default_owner),
|
||||
]
|
17
cookbook/migrations/0112_remove_synclog_space.py
Normal file
17
cookbook/migrations/0112_remove_synclog_space.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-16 23:21
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0111_space_created_by'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='synclog',
|
||||
name='space',
|
||||
),
|
||||
]
|
21
cookbook/migrations/0113_auto_20210317_2017.py
Normal file
21
cookbook/migrations/0113_auto_20210317_2017.py
Normal file
@ -0,0 +1,21 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-17 19:17
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0112_remove_synclog_space'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='shoppinglistentry',
|
||||
name='space',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='shoppinglistrecipe',
|
||||
name='space',
|
||||
),
|
||||
]
|
31
cookbook/migrations/0114_importlog.py
Normal file
31
cookbook/migrations/0114_importlog.py
Normal file
@ -0,0 +1,31 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-18 17:23
|
||||
|
||||
import cookbook.models
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('cookbook', '0113_auto_20210317_2017'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ImportLog',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('type', models.CharField(max_length=32)),
|
||||
('running', models.BooleanField(default=True)),
|
||||
('msg', models.TextField(default='')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
('keyword', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='cookbook.keyword')),
|
||||
('space', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space')),
|
||||
],
|
||||
bases=(models.Model, cookbook.models.PermissionModelMixin),
|
||||
),
|
||||
]
|
31
cookbook/migrations/0115_telegrambot.py
Normal file
31
cookbook/migrations/0115_telegrambot.py
Normal 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),
|
||||
),
|
||||
]
|
41
cookbook/migrations/0116_auto_20210319_0012.py
Normal file
41
cookbook/migrations/0116_auto_20210319_0012.py
Normal 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),
|
||||
]
|
18
cookbook/migrations/0117_space_max_recipes.py
Normal file
18
cookbook/migrations/0117_space_max_recipes.py
Normal 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),
|
||||
),
|
||||
]
|
25
cookbook/migrations/0118_auto_20210406_1805.py
Normal file
25
cookbook/migrations/0118_auto_20210406_1805.py
Normal 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),
|
||||
]
|
15
cookbook/migrations/0119_auto_20210411_2101.py
Normal file
15
cookbook/migrations/0119_auto_20210411_2101.py
Normal 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(),
|
||||
]
|
34
cookbook/migrations/0120_bookmarklet.py
Normal file
34
cookbook/migrations/0120_bookmarklet.py
Normal 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),
|
||||
),
|
||||
]
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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)
|
||||
|
BIN
cookbook/static/assets/apple-touch-icon.png
Normal file
BIN
cookbook/static/assets/apple-touch-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.5 KiB |
BIN
cookbook/static/assets/favicon-16x16.png
Normal file
BIN
cookbook/static/assets/favicon-16x16.png
Normal file
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
Loading…
Reference in New Issue
Block a user