Merge branch 'develop' into feature/vue
This commit is contained in:
commit
5bb20bd479
@ -1,5 +1,11 @@
|
||||
from django.contrib import admin
|
||||
from .models import *
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class SpaceAdmin(admin.ModelAdmin):
|
||||
@ -10,7 +16,10 @@ admin.site.register(Space, SpaceAdmin)
|
||||
|
||||
|
||||
class UserPreferenceAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'theme', 'nav_color', 'default_page', 'search_style', 'comments')
|
||||
list_display = (
|
||||
'name', 'theme', 'nav_color',
|
||||
'default_page', 'search_style', 'comments'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def name(obj):
|
||||
@ -133,7 +142,10 @@ admin.site.register(ViewLog, ViewLogAdmin)
|
||||
|
||||
|
||||
class InviteLinkAdmin(admin.ModelAdmin):
|
||||
list_display = ('username', 'group', 'valid_until', 'created_by', 'created_at', 'used_by')
|
||||
list_display = (
|
||||
'username', 'group', 'valid_until',
|
||||
'created_by', 'created_at', 'used_by'
|
||||
)
|
||||
|
||||
|
||||
admin.site.register(InviteLink, InviteLinkAdmin)
|
||||
|
@ -1,18 +1,26 @@
|
||||
import django_filters
|
||||
from django.conf import settings
|
||||
from django.contrib.postgres.search import TrigramSimilarity
|
||||
from django.db.models import Q
|
||||
from cookbook.forms import MultiSelectWidget
|
||||
from cookbook.models import Recipe, Keyword, Food, ShoppingList
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from cookbook.forms import MultiSelectWidget
|
||||
from cookbook.models import Food, Keyword, Recipe, ShoppingList
|
||||
|
||||
|
||||
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'))
|
||||
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')
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def filter_keywords(queryset, name, value):
|
||||
@ -27,16 +35,20 @@ class RecipeFilter(django_filters.FilterSet):
|
||||
if not name == 'foods':
|
||||
return queryset
|
||||
for x in value:
|
||||
queryset = queryset.filter(steps__ingredients__food__name=x).distinct()
|
||||
queryset = queryset.filter(
|
||||
steps__ingredients__food__name=x
|
||||
).distinct()
|
||||
return queryset
|
||||
|
||||
@staticmethod
|
||||
def filter_name(queryset, name, value):
|
||||
if not name == 'name':
|
||||
return queryset
|
||||
if settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql_psycopg2':
|
||||
queryset = queryset.annotate(similarity=TrigramSimilarity('name', value), ).filter(
|
||||
Q(similarity__gt=0.1) | Q(name__icontains=value)).order_by('-similarity')
|
||||
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__icontains=value)) \
|
||||
.order_by('-similarity')
|
||||
else:
|
||||
queryset = queryset.filter(name__icontains=value)
|
||||
return queryset
|
||||
|
@ -3,7 +3,9 @@ from django.forms import widgets
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from emoji_picker.widgets import EmojiPickerTextInput
|
||||
|
||||
from .models import *
|
||||
from .models import (Comment, Food, InviteLink, Keyword, MealPlan, Recipe,
|
||||
RecipeBook, RecipeBookEntry, Storage, Sync, Unit, User,
|
||||
UserPreference)
|
||||
|
||||
|
||||
class SelectWidget(widgets.Select):
|
||||
@ -16,7 +18,8 @@ class MultiSelectWidget(widgets.SelectMultiple):
|
||||
js = ('custom/js/form_multiselect.js',)
|
||||
|
||||
|
||||
# yes there are some stupid browsers that still dont support this but i dont support people using these browsers
|
||||
# Yes there are some stupid browsers that still dont support this but
|
||||
# I dont support people using these browsers.
|
||||
class DateWidget(forms.DateInput):
|
||||
input_type = 'date'
|
||||
|
||||
@ -30,20 +33,26 @@ class UserPreferenceForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = UserPreference
|
||||
fields = ('default_unit', 'use_fractions', 'theme', 'nav_color', 'sticky_navbar', 'default_page', 'show_recent', 'search_style', 'plan_share', 'ingredient_decimals', 'shopping_auto_sync', 'comments')
|
||||
fields = (
|
||||
'default_unit', 'use_fractions', 'theme', 'nav_color',
|
||||
'sticky_navbar', 'default_page', 'show_recent', 'search_style',
|
||||
'plan_share', 'ingredient_decimals', 'shopping_auto_sync',
|
||||
'comments'
|
||||
)
|
||||
|
||||
help_texts = {
|
||||
'nav_color': _('Color of the top navigation bar. Not all colors work with all themes, just try them out!'),
|
||||
'default_unit': _('Default Unit to be used when inserting a new ingredient into a recipe.'),
|
||||
'use_fractions': _('Enables support for fractions in ingredient amounts (e.g. convert decimals to fractions automatically)'),
|
||||
'plan_share': _('Users with whom newly created meal plan/shopping list entries should be shared by default.'),
|
||||
'show_recent': _('Show recently viewed recipes on search page.'),
|
||||
'ingredient_decimals': _('Number of decimals to round ingredients.'),
|
||||
'comments': _('If you want to be able to create and see comments underneath recipes.'),
|
||||
'nav_color': _('Color of the top navigation bar. Not all colors work with all themes, just try them out!'), # noqa: E501
|
||||
'default_unit': _('Default Unit to be used when inserting a new ingredient into a recipe.'), # noqa: E501
|
||||
'use_fractions': _('Enables support for fractions in ingredient amounts (e.g. convert decimals to fractions automatically)'), # noqa: E501
|
||||
'plan_share': _('Users with whom newly created meal plan/shopping list entries should be shared by default.'), # noqa: E501
|
||||
'show_recent': _('Show recently viewed recipes on search page.'), # noqa: E501
|
||||
'ingredient_decimals': _('Number of decimals to round ingredients.'), # noqa: E501
|
||||
'comments': _('If you want to be able to create and see comments underneath recipes.'), # noqa: E501
|
||||
'shopping_auto_sync': _(
|
||||
'Setting to 0 will disable auto sync. When viewing a shopping list the list is updated every set seconds to sync changes someone else might have made. Useful when shopping with multiple people but might use a little bit '
|
||||
'of mobile data. If lower than instance limit it is reset when saving.'),
|
||||
'sticky_navbar': _('Makes the navbar stick to the top of the page.')
|
||||
'Setting to 0 will disable auto sync. When viewing a shopping list the list is updated every set seconds to sync changes someone else might have made. Useful when shopping with multiple people but might use a little bit ' # noqa: E501
|
||||
'of mobile data. If lower than instance limit it is reset when saving.' # noqa: E501
|
||||
),
|
||||
'sticky_navbar': _('Makes the navbar stick to the top of the page.') # noqa: E501
|
||||
}
|
||||
|
||||
widgets = {
|
||||
@ -59,18 +68,25 @@ class UserNameForm(forms.ModelForm):
|
||||
fields = ('first_name', 'last_name')
|
||||
|
||||
help_texts = {
|
||||
'first_name': _('Both fields are optional. If none are given the username will be displayed instead')
|
||||
'first_name': _('Both fields are optional. If none are given the username will be displayed instead') # noqa: E501
|
||||
}
|
||||
|
||||
|
||||
class ExternalRecipeForm(forms.ModelForm):
|
||||
file_path = forms.CharField(disabled=True, required=False)
|
||||
storage = forms.ModelChoiceField(queryset=Storage.objects.all(), disabled=True, required=False)
|
||||
storage = forms.ModelChoiceField(
|
||||
queryset=Storage.objects.all(),
|
||||
disabled=True,
|
||||
required=False
|
||||
)
|
||||
file_uid = forms.CharField(disabled=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = Recipe
|
||||
fields = ('name', 'keywords', 'working_time', 'waiting_time', 'file_path', 'storage', 'file_uid')
|
||||
fields = (
|
||||
'name', 'keywords', 'working_time', 'waiting_time',
|
||||
'file_path', 'storage', 'file_uid'
|
||||
)
|
||||
|
||||
labels = {
|
||||
'name': _('Name'),
|
||||
@ -88,7 +104,10 @@ class InternalRecipeForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Recipe
|
||||
fields = ('name', 'image', 'working_time', 'waiting_time', 'servings', 'keywords')
|
||||
fields = (
|
||||
'name', 'image', 'working_time',
|
||||
'waiting_time', 'servings', 'keywords'
|
||||
)
|
||||
|
||||
labels = {
|
||||
'name': _('Name'),
|
||||
@ -106,7 +125,7 @@ class ShoppingForm(forms.Form):
|
||||
widget=MultiSelectWidget
|
||||
)
|
||||
markdown_format = forms.BooleanField(
|
||||
help_text=_('Include <code>- [ ]</code> in list for easier usage in markdown based documents.'),
|
||||
help_text=_('Include <code>- [ ]</code> in list for easier usage in markdown based documents.'), # noqa: E501
|
||||
required=False,
|
||||
initial=False
|
||||
)
|
||||
@ -128,7 +147,10 @@ class ExportForm(forms.Form):
|
||||
|
||||
|
||||
class ImportForm(forms.Form):
|
||||
recipe = forms.CharField(widget=forms.Textarea, help_text=_('Simply paste a JSON export into this textarea and click import.'))
|
||||
recipe = forms.CharField(
|
||||
widget=forms.Textarea,
|
||||
help_text=_('Simply paste a JSON export into this textarea and click import.') # noqa: E501
|
||||
)
|
||||
|
||||
|
||||
class UnitMergeForm(forms.Form):
|
||||
@ -195,21 +217,31 @@ class FoodForm(forms.ModelForm):
|
||||
|
||||
|
||||
class StorageForm(forms.ModelForm):
|
||||
username = forms.CharField(widget=forms.TextInput(attrs={'autocomplete': 'new-password'}), required=False)
|
||||
password = forms.CharField(widget=forms.TextInput(attrs={'autocomplete': 'new-password', 'type': 'password'}),
|
||||
username = forms.CharField(
|
||||
widget=forms.TextInput(attrs={'autocomplete': 'new-password'}),
|
||||
required=False
|
||||
)
|
||||
password = forms.CharField(
|
||||
widget=forms.TextInput(
|
||||
attrs={'autocomplete': 'new-password', 'type': 'password'}
|
||||
),
|
||||
required=False,
|
||||
help_text=_('Leave empty for dropbox and enter app password for nextcloud.'))
|
||||
token = forms.CharField(widget=forms.TextInput(attrs={'autocomplete': 'new-password', 'type': 'password'}),
|
||||
help_text=_('Leave empty for dropbox and enter app password for nextcloud.') # noqa: E501
|
||||
)
|
||||
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.'))
|
||||
help_text=_('Leave empty for nextcloud and enter api token for dropbox.') # noqa: E501
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Storage
|
||||
fields = ('name', 'method', 'username', 'password', 'token', 'url')
|
||||
|
||||
help_texts = {
|
||||
'url': _(
|
||||
'Leave empty for dropbox and enter only base url for nextcloud (<code>/remote.php/webdav/</code> is added automatically)'),
|
||||
'url': _('Leave empty for dropbox and enter only base url for nextcloud (<code>/remote.php/webdav/</code> is added automatically)'), # noqa: E501
|
||||
}
|
||||
|
||||
|
||||
@ -229,8 +261,11 @@ class SyncForm(forms.ModelForm):
|
||||
|
||||
class BatchEditForm(forms.Form):
|
||||
search = forms.CharField(label=_('Search String'))
|
||||
keywords = forms.ModelMultipleChoiceField(queryset=Keyword.objects.all().order_by('id'), required=False,
|
||||
widget=MultiSelectWidget)
|
||||
keywords = forms.ModelMultipleChoiceField(
|
||||
queryset=Keyword.objects.all().order_by('id'),
|
||||
required=False,
|
||||
widget=MultiSelectWidget
|
||||
)
|
||||
|
||||
|
||||
class ImportRecipeForm(forms.ModelForm):
|
||||
@ -260,20 +295,29 @@ class MealPlanForm(forms.ModelForm):
|
||||
cleaned_data = super(MealPlanForm, self).clean()
|
||||
|
||||
if cleaned_data['title'] == '' and cleaned_data['recipe'] is None:
|
||||
raise forms.ValidationError(_('You must provide at least a recipe or a title.'))
|
||||
raise forms.ValidationError(
|
||||
_('You must provide at least a recipe or a title.')
|
||||
)
|
||||
|
||||
return cleaned_data
|
||||
|
||||
class Meta:
|
||||
model = MealPlan
|
||||
fields = ('recipe', 'title', 'meal_type', 'note', 'servings', 'date', 'shared')
|
||||
fields = (
|
||||
'recipe', 'title', 'meal_type', 'note',
|
||||
'servings', 'date', 'shared'
|
||||
)
|
||||
|
||||
help_texts = {
|
||||
'shared': _('You can list default users to share recipes with in the settings.'),
|
||||
'note': _('You can use markdown to format this field. See the <a href="/docs/markdown/">docs here</a>')
|
||||
'shared': _('You can list default users to share recipes with in the settings.'), # noqa: E501
|
||||
'note': _('You can use markdown to format this field. See the <a href="/docs/markdown/">docs here</a>') # noqa: E501
|
||||
}
|
||||
|
||||
widgets = {'recipe': SelectWidget, 'date': DateWidget, 'shared': MultiSelectWidget}
|
||||
widgets = {
|
||||
'recipe': SelectWidget,
|
||||
'date': DateWidget,
|
||||
'shared': MultiSelectWidget
|
||||
}
|
||||
|
||||
|
||||
class InviteLinkForm(forms.ModelForm):
|
||||
@ -281,11 +325,19 @@ class InviteLinkForm(forms.ModelForm):
|
||||
model = InviteLink
|
||||
fields = ('username', 'group', 'valid_until')
|
||||
help_texts = {
|
||||
'username': _('A username is not required, if left blank the new user can choose one.')
|
||||
'username': _('A username is not required, if left blank the new user can choose one.') # noqa: E501
|
||||
}
|
||||
|
||||
|
||||
class UserCreateForm(forms.Form):
|
||||
name = forms.CharField(label='Username')
|
||||
password = forms.CharField(widget=forms.TextInput(attrs={'autocomplete': 'new-password', 'type': 'password'}))
|
||||
password_confirm = forms.CharField(widget=forms.TextInput(attrs={'autocomplete': 'new-password', 'type': 'password'}))
|
||||
password = forms.CharField(
|
||||
widget=forms.TextInput(
|
||||
attrs={'autocomplete': 'new-password', 'type': 'password'}
|
||||
)
|
||||
)
|
||||
password_confirm = forms.CharField(
|
||||
widget=forms.TextInput(
|
||||
attrs={'autocomplete': 'new-password', 'type': 'password'}
|
||||
)
|
||||
)
|
||||
|
@ -1 +1,5 @@
|
||||
from cookbook.helper.dal import *
|
||||
import cookbook.helper.dal
|
||||
|
||||
__all__ = [
|
||||
'dal',
|
||||
]
|
||||
|
@ -1,14 +1,16 @@
|
||||
from cookbook.models import Food, Keyword, Recipe, Unit
|
||||
|
||||
from dal import autocomplete
|
||||
|
||||
from cookbook.models import Keyword, Recipe, Unit, Food
|
||||
|
||||
class BaseAutocomplete(autocomplete.Select2QuerySetView):
|
||||
model = None
|
||||
|
||||
class KeywordAutocomplete(autocomplete.Select2QuerySetView):
|
||||
def get_queryset(self):
|
||||
if not self.request.user.is_authenticated:
|
||||
return Keyword.objects.none()
|
||||
return self.model.objects.none()
|
||||
|
||||
qs = Keyword.objects.all()
|
||||
qs = self.model.objects.all()
|
||||
|
||||
if self.q:
|
||||
qs = qs.filter(name__istartswith=self.q)
|
||||
@ -16,40 +18,17 @@ class KeywordAutocomplete(autocomplete.Select2QuerySetView):
|
||||
return qs
|
||||
|
||||
|
||||
class IngredientsAutocomplete(autocomplete.Select2QuerySetView):
|
||||
def get_queryset(self):
|
||||
if not self.request.user.is_authenticated:
|
||||
return Food.objects.none()
|
||||
|
||||
qs = Food.objects.all()
|
||||
|
||||
if self.q:
|
||||
qs = qs.filter(name__icontains=self.q)
|
||||
|
||||
return qs
|
||||
class KeywordAutocomplete(BaseAutocomplete):
|
||||
model = Keyword
|
||||
|
||||
|
||||
class RecipeAutocomplete(autocomplete.Select2QuerySetView):
|
||||
def get_queryset(self):
|
||||
if not self.request.user.is_authenticated:
|
||||
return Recipe.objects.none()
|
||||
|
||||
qs = Recipe.objects.all()
|
||||
|
||||
if self.q:
|
||||
qs = qs.filter(name__icontains=self.q)
|
||||
|
||||
return qs
|
||||
class IngredientsAutocomplete(BaseAutocomplete):
|
||||
model = Food
|
||||
|
||||
|
||||
class UnitAutocomplete(autocomplete.Select2QuerySetView):
|
||||
def get_queryset(self):
|
||||
if not self.request.user.is_authenticated:
|
||||
return Unit.objects.none()
|
||||
class RecipeAutocomplete(BaseAutocomplete):
|
||||
model = Recipe
|
||||
|
||||
qs = Unit.objects.all()
|
||||
|
||||
if self.q:
|
||||
qs = qs.filter(name__icontains=self.q)
|
||||
|
||||
return qs
|
||||
class UnitAutocomplete(BaseAutocomplete):
|
||||
model = Unit
|
||||
|
@ -1,11 +1,12 @@
|
||||
import unicodedata
|
||||
import string
|
||||
import unicodedata
|
||||
|
||||
|
||||
def parse_fraction(x):
|
||||
if len(x) == 1 and 'fraction' in unicodedata.decomposition(x):
|
||||
frac_split = unicodedata.decomposition(x[-1:]).split()
|
||||
return float((frac_split[1]).replace('003', '')) / float((frac_split[3]).replace('003', ''))
|
||||
return (float((frac_split[1]).replace('003', ''))
|
||||
/ float((frac_split[3]).replace('003', '')))
|
||||
else:
|
||||
frac_split = x.split('/')
|
||||
if not len(frac_split) == 2:
|
||||
@ -22,7 +23,17 @@ def parse_amount(x):
|
||||
|
||||
did_check_frac = False
|
||||
end = 0
|
||||
while end < len(x) and (x[end] in string.digits or ((x[end] == '.' or x[end] == ',') and end + 1 < len(x) and x[end + 1] in string.digits)):
|
||||
while (
|
||||
end < len(x)
|
||||
and (
|
||||
x[end] in string.digits
|
||||
or (
|
||||
(x[end] == '.' or x[end] == ',')
|
||||
and end + 1 < len(x)
|
||||
and x[end + 1] in string.digits
|
||||
)
|
||||
)
|
||||
):
|
||||
end += 1
|
||||
if end > 0:
|
||||
amount = float(x[:end].replace(',', '.'))
|
||||
@ -70,13 +81,13 @@ def parse_ingredient(tokens):
|
||||
while not tokens[start].startswith('(') and not start == 0:
|
||||
start -= 1
|
||||
if start == 0:
|
||||
# the whole list is wrapped in brackets -> assume it is an error (e.g. assumed first argument was the unit)
|
||||
# the whole list is wrapped in brackets -> assume it is an error (e.g. assumed first argument was the unit) # noqa: E501
|
||||
raise ValueError
|
||||
elif start < 0:
|
||||
# no opening bracket anywhere -> just ignore the last bracket
|
||||
ingredient, note = parse_ingredient_with_comma(tokens)
|
||||
else:
|
||||
# opening bracket found -> split in ingredient and note, remove brackets from note
|
||||
# opening bracket found -> split in ingredient and note, remove brackets from note # noqa: E501
|
||||
note = ' '.join(tokens[start:])[1:-1]
|
||||
ingredient = ' '.join(tokens[:start])
|
||||
else:
|
||||
@ -99,19 +110,20 @@ def parse(x):
|
||||
try:
|
||||
# try to parse first argument as amount
|
||||
amount, unit = parse_amount(tokens[0])
|
||||
# only try to parse second argument as amount if there are at least three arguments
|
||||
# if it already has a unit there can't be a fraction for the amount
|
||||
# only try to parse second argument as amount if there are at least
|
||||
# three arguments if it already has a unit there can't be
|
||||
# a fraction for the amount
|
||||
if len(tokens) > 2:
|
||||
try:
|
||||
if not unit == '':
|
||||
# a unit is already found, no need to try the second argument for a fraction
|
||||
# probably not the best method to do it, but I didn't want to make an if check and paste the exact same thing in the else as already is in the except
|
||||
# a unit is already found, no need to try the second argument for a fraction # noqa: E501
|
||||
# probably not the best method to do it, but I didn't want to make an if check and paste the exact same thing in the else as already is in the except # noqa: E501
|
||||
raise ValueError
|
||||
# try to parse second argument as amount and add that, in case of '2 1/2' or '2 ½'
|
||||
# try to parse second argument as amount and add that, in case of '2 1/2' or '2 ½' # noqa: E501
|
||||
amount += parse_fraction(tokens[1])
|
||||
# assume that units can't end with a comma
|
||||
if len(tokens) > 3 and not tokens[2].endswith(','):
|
||||
# try to use third argument as unit and everything else as ingredient, use everything as ingredient if it fails
|
||||
# try to use third argument as unit and everything else as ingredient, use everything as ingredient if it fails # noqa: E501
|
||||
try:
|
||||
ingredient, note = parse_ingredient(tokens[3:])
|
||||
unit = tokens[2]
|
||||
@ -122,7 +134,7 @@ def parse(x):
|
||||
except ValueError:
|
||||
# assume that units can't end with a comma
|
||||
if not tokens[1].endswith(','):
|
||||
# try to use second argument as unit and everything else as ingredient, use everything as ingredient if it fails
|
||||
# try to use second argument as unit and everything else as ingredient, use everything as ingredient if it fails # noqa: E501
|
||||
try:
|
||||
ingredient, note = parse_ingredient(tokens[2:])
|
||||
unit = tokens[1]
|
||||
@ -131,11 +143,13 @@ def parse(x):
|
||||
else:
|
||||
ingredient, note = parse_ingredient(tokens[1:])
|
||||
else:
|
||||
# only two arguments, first one is the amount which means this is the ingredient
|
||||
# only two arguments, first one is the amount
|
||||
# which means this is the ingredient
|
||||
ingredient = tokens[1]
|
||||
except ValueError:
|
||||
try:
|
||||
# can't parse first argument as amount -> no unit -> parse everything as ingredient
|
||||
# can't parse first argument as amount
|
||||
# -> no unit -> parse everything as ingredient
|
||||
ingredient, note = parse_ingredient(tokens)
|
||||
except ValueError:
|
||||
ingredient = ' '.join(tokens[1:])
|
||||
|
@ -1,5 +1,4 @@
|
||||
import markdown
|
||||
|
||||
from markdown.treeprocessors import Treeprocessor
|
||||
|
||||
|
||||
@ -21,4 +20,8 @@ class StyleTreeprocessor(Treeprocessor):
|
||||
|
||||
class MarkdownFormatExtension(markdown.Extension):
|
||||
def extendMarkdown(self, md, md_globals):
|
||||
md.treeprocessors.register(StyleTreeprocessor(), 'StyleTreeprocessor', 10)
|
||||
md.treeprocessors.register(
|
||||
StyleTreeprocessor(),
|
||||
'StyleTreeprocessor',
|
||||
10
|
||||
)
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""A more liberal autolinker
|
||||
"""
|
||||
A more liberal autolinker
|
||||
|
||||
Inspired by Django's urlize function.
|
||||
|
||||
@ -45,8 +46,10 @@ URLIZE_RE = '(%s)' % '|'.join([
|
||||
r'[^(<\s]+\.(?:com|net|org)\b',
|
||||
])
|
||||
|
||||
|
||||
class UrlizePattern(markdown.inlinepatterns.Pattern):
|
||||
""" Return a link Element given an autolink (`http://example/com`). """
|
||||
|
||||
def handleMatch(self, m):
|
||||
url = m.group(2)
|
||||
|
||||
@ -55,8 +58,8 @@ class UrlizePattern(markdown.inlinepatterns.Pattern):
|
||||
|
||||
text = url
|
||||
|
||||
if not url.split('://')[0] in ('http','https','ftp'):
|
||||
if '@' in url and not '/' in url:
|
||||
if not url.split('://')[0] in ('http', 'https', 'ftp'):
|
||||
if '@' in url and '/' not in url:
|
||||
url = 'mailto:' + url
|
||||
else:
|
||||
url = 'http://' + url
|
||||
@ -66,6 +69,7 @@ class UrlizePattern(markdown.inlinepatterns.Pattern):
|
||||
el.text = markdown.util.AtomicString(text)
|
||||
return el
|
||||
|
||||
|
||||
class UrlizeExtension(markdown.Extension):
|
||||
""" Urlize Extension for Python-Markdown. """
|
||||
|
||||
@ -73,9 +77,12 @@ class UrlizeExtension(markdown.Extension):
|
||||
""" Replace autolink with UrlizePattern """
|
||||
md.inlinePatterns['autolink'] = UrlizePattern(URLIZE_RE, md)
|
||||
|
||||
|
||||
def makeExtension(*args, **kwargs):
|
||||
return UrlizeExtension(*args, **kwargs)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import doctest
|
||||
|
||||
doctest.testmod()
|
||||
|
@ -1,5 +1,4 @@
|
||||
# Permission Config
|
||||
from cookbook.helper.permission_helper import CustomIsUser, CustomIsOwner, CustomIsAdmin, CustomIsGuest
|
||||
from cookbook.helper.permission_helper import CustomIsUser
|
||||
|
||||
|
||||
class PermissionConfig:
|
||||
|
@ -1,20 +1,16 @@
|
||||
"""
|
||||
Source: https://djangosnippets.org/snippets/1703/
|
||||
"""
|
||||
from cookbook.models import ShareLink
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import user_passes_test
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from django.utils.translation import gettext as _
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import reverse_lazy, reverse
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils.translation import gettext as _
|
||||
from rest_framework import permissions
|
||||
from rest_framework.permissions import SAFE_METHODS
|
||||
|
||||
from cookbook.models import ShareLink
|
||||
|
||||
|
||||
# Helper Functions
|
||||
|
||||
def get_allowed_groups(groups_required):
|
||||
"""
|
||||
@ -34,8 +30,8 @@ def get_allowed_groups(groups_required):
|
||||
def has_group_permission(user, groups):
|
||||
"""
|
||||
Tests if a given user is member of a certain group (or any higher group)
|
||||
Superusers always bypass permission checks. Unauthenticated users cant be member of any
|
||||
group thus always return false.
|
||||
Superusers always bypass permission checks.
|
||||
Unauthenticated users cant be member of any group thus always return false.
|
||||
:param user: django auth user object
|
||||
:param groups: list or tuple of groups the user should be checked for
|
||||
:return: True if user is in allowed groups, false otherwise
|
||||
@ -44,7 +40,8 @@ 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 (user.is_superuser
|
||||
| bool(user.groups.filter(name__in=groups_allowed))):
|
||||
return True
|
||||
return False
|
||||
|
||||
@ -52,13 +49,15 @@ def has_group_permission(user, groups):
|
||||
def is_object_owner(user, obj):
|
||||
"""
|
||||
Tests if a given user is the owner of a given object
|
||||
test performed by checking user against the objects user and create_by field (if exists)
|
||||
test performed by checking user against the objects user
|
||||
and create_by field (if exists)
|
||||
superusers bypass all checks, unauthenticated users cannot own anything
|
||||
:param user django auth user object
|
||||
: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
|
||||
# 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:
|
||||
@ -81,7 +80,8 @@ def is_object_shared(user, obj):
|
||||
:param obj any object that should be tested
|
||||
:return: true if user is shared for object, false otherwise
|
||||
"""
|
||||
# TODO this could be improved/cleaned up by adding share checks for relevant objects
|
||||
# TODO this could be improved/cleaned up by adding
|
||||
# share checks for relevant objects
|
||||
if not user.is_authenticated:
|
||||
return False
|
||||
if user.is_superuser:
|
||||
@ -94,10 +94,14 @@ def share_link_valid(recipe, share):
|
||||
Verifies the validity of a share uuid
|
||||
:param recipe: recipe object
|
||||
:param share: share uuid
|
||||
:return: true if a share link with the given recipe and uuid exists, false otherwise
|
||||
: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
|
||||
|
||||
@ -106,8 +110,8 @@ def share_link_valid(recipe, share):
|
||||
|
||||
def group_required(*groups_required):
|
||||
"""
|
||||
Decorator that tests the requesting user to be member of at least one of the provided groups
|
||||
or higher level groups
|
||||
Decorator that tests the requesting user to be member
|
||||
of at least one of the provided groups or higher level groups
|
||||
:param groups_required: list of required groups
|
||||
:return: true if member of group, false otherwise
|
||||
"""
|
||||
@ -127,24 +131,40 @@ 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!'))
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.ERROR,
|
||||
_('You do not have the required permissions to view this page!') # noqa: E501
|
||||
)
|
||||
return HttpResponseRedirect(reverse_lazy('index'))
|
||||
|
||||
return super(GroupRequiredMixin, self).dispatch(request, *args, **kwargs)
|
||||
return super(GroupRequiredMixin, self) \
|
||||
.dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
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('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('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!'))
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.ERROR,
|
||||
_('You cannot interact with this object as it is not owned by you!') # noqa: E501
|
||||
)
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
|
||||
return super(OwnerRequiredMixin, self).dispatch(request, *args, **kwargs)
|
||||
return super(OwnerRequiredMixin, self) \
|
||||
.dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
# Django Rest Framework Permission classes
|
||||
@ -155,7 +175,7 @@ class CustomIsOwner(permissions.BasePermission):
|
||||
verifies user has ownership over object
|
||||
(either user or created_by or user is request user)
|
||||
"""
|
||||
message = _('You cannot interact with this object as it is not owned by you!')
|
||||
message = _('You cannot interact with this object as it is not owned by you!') # noqa: E501
|
||||
|
||||
def has_permission(self, request, view):
|
||||
return request.user.is_authenticated
|
||||
@ -164,12 +184,13 @@ class CustomIsOwner(permissions.BasePermission):
|
||||
return is_object_owner(request.user, obj)
|
||||
|
||||
|
||||
class CustomIsShared(permissions.BasePermission): # TODO function duplicate/too similar name
|
||||
# TODO function duplicate/too similar name
|
||||
class CustomIsShared(permissions.BasePermission):
|
||||
"""
|
||||
Custom permission class for django rest framework views
|
||||
verifies user is shared for the object he is trying to access
|
||||
"""
|
||||
message = _('You cannot interact with this object as it is not owned by you!')
|
||||
message = _('You cannot interact with this object as it is not owned by you!') # noqa: E501
|
||||
|
||||
def has_permission(self, request, view):
|
||||
return request.user.is_authenticated
|
||||
|
@ -1,18 +1,16 @@
|
||||
import json
|
||||
import random
|
||||
import re
|
||||
import unicodedata
|
||||
from json import JSONDecodeError
|
||||
|
||||
import microdata
|
||||
from bs4 import BeautifulSoup
|
||||
from cookbook.helper.ingredient_parser import parse as parse_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 cookbook.models import Keyword
|
||||
from cookbook.helper.ingredient_parser import parse as parse_ingredient
|
||||
|
||||
|
||||
def get_from_html(html_text, url):
|
||||
soup = BeautifulSoup(html_text, "html.parser")
|
||||
@ -31,10 +29,16 @@ def get_from_html(html_text, url):
|
||||
if '@type' in x and x['@type'] == 'Recipe':
|
||||
ld_json_item = x
|
||||
|
||||
if '@type' in ld_json_item and ld_json_item['@type'] == 'Recipe':
|
||||
if ('@type' in ld_json_item
|
||||
and ld_json_item['@type'] == 'Recipe'):
|
||||
return find_recipe_json(ld_json_item, url)
|
||||
except JSONDecodeError as e:
|
||||
return JsonResponse({'error': True, 'msg': _('The requested site provided malformed data and cannot be read.')}, status=400)
|
||||
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)
|
||||
@ -43,14 +47,19 @@ def get_from_html(html_text, url):
|
||||
if 'schema.org/Recipe' in str(md_json['type']):
|
||||
return 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.')}, status=400)
|
||||
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]
|
||||
except:
|
||||
except Exception:
|
||||
ld_json['name'] = 'ERROR'
|
||||
|
||||
# some sites use ingredients instead of recipeIngredients
|
||||
@ -59,8 +68,9 @@ def find_recipe_json(ld_json, url):
|
||||
|
||||
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(',')
|
||||
if (len(ld_json['recipeIngredient']) == 1
|
||||
and len(ld_json['recipeIngredient'][0]) > 30):
|
||||
ld_json['recipeIngredient'] = ld_json['recipeIngredient'][0].split(',') # noqa: E501
|
||||
|
||||
for x in ld_json['recipeIngredient']:
|
||||
if '\n' in x:
|
||||
@ -71,13 +81,41 @@ def find_recipe_json(ld_json, url):
|
||||
ingredients = []
|
||||
|
||||
for x in ld_json['recipeIngredient']:
|
||||
if x.replace(' ','') != '':
|
||||
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:
|
||||
ingredients.append({'amount': 0, 'unit': {'text': "", 'id': random.randrange(10000, 99999)}, 'ingredient': {'text': x, 'id': random.randrange(10000, 99999)}, "note": "", 'original': x})
|
||||
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:
|
||||
ingredients.append(
|
||||
{
|
||||
'amount': 0,
|
||||
'unit': {
|
||||
'text': '',
|
||||
'id': random.randrange(10000, 99999)
|
||||
},
|
||||
'ingredient': {
|
||||
'text': x,
|
||||
'id': random.randrange(10000, 99999)
|
||||
},
|
||||
'note': '',
|
||||
'original': x
|
||||
}
|
||||
)
|
||||
|
||||
ld_json['recipeIngredient'] = ingredients
|
||||
else:
|
||||
@ -91,7 +129,9 @@ def find_recipe_json(ld_json, url):
|
||||
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]:
|
||||
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
|
||||
@ -126,10 +166,10 @@ def find_recipe_json(ld_json, url):
|
||||
instructions += str(i)
|
||||
ld_json['recipeInstructions'] = instructions
|
||||
|
||||
ld_json['recipeInstructions'] = re.sub(r'\n\s*\n', '\n\n', ld_json['recipeInstructions'])
|
||||
ld_json['recipeInstructions'] = re.sub(' +', ' ', ld_json['recipeInstructions'])
|
||||
ld_json['recipeInstructions'] = ld_json['recipeInstructions'].replace('<p>', '')
|
||||
ld_json['recipeInstructions'] = ld_json['recipeInstructions'].replace('</p>', '')
|
||||
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'] = ''
|
||||
|
||||
@ -149,9 +189,14 @@ def find_recipe_json(ld_json, url):
|
||||
|
||||
if 'cookTime' in ld_json:
|
||||
try:
|
||||
if type(ld_json['cookTime']) == list and len(ld_json['cookTime']) > 0:
|
||||
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)
|
||||
ld_json['cookTime'] = round(
|
||||
parse_duration(
|
||||
ld_json['cookTime']
|
||||
).seconds / 60
|
||||
)
|
||||
except TypeError:
|
||||
ld_json['cookTime'] = 0
|
||||
else:
|
||||
@ -159,16 +204,24 @@ def find_recipe_json(ld_json, url):
|
||||
|
||||
if 'prepTime' in ld_json:
|
||||
try:
|
||||
if type(ld_json['prepTime']) == list and len(ld_json['prepTime']) > 0:
|
||||
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)
|
||||
ld_json['prepTime'] = round(
|
||||
parse_duration(
|
||||
ld_json['prepTime']
|
||||
).seconds / 60
|
||||
)
|
||||
except TypeError:
|
||||
ld_json['prepTime'] = 0
|
||||
else:
|
||||
ld_json['prepTime'] = 0
|
||||
|
||||
for key in list(ld_json):
|
||||
if key not in ['prepTime', 'cookTime', 'image', 'recipeInstructions', 'keywords', 'name', 'recipeIngredient']:
|
||||
if key not in [
|
||||
'prepTime', 'cookTime', 'image', 'recipeInstructions',
|
||||
'keywords', 'name', 'recipeIngredient'
|
||||
]:
|
||||
ld_json.pop(key, None)
|
||||
|
||||
return JsonResponse(ld_json)
|
||||
|
@ -1,10 +1,9 @@
|
||||
import bleach
|
||||
import markdown as md
|
||||
from bleach_whitelist import markdown_tags, markdown_attrs
|
||||
from jinja2 import Template, TemplateSyntaxError
|
||||
|
||||
from bleach_whitelist import markdown_attrs, markdown_tags
|
||||
from cookbook.helper.mdx_attributes import MarkdownFormatExtension
|
||||
from cookbook.helper.mdx_urlize import UrlizeExtension
|
||||
from jinja2 import Template, TemplateSyntaxError
|
||||
|
||||
|
||||
class IngredientObject(object):
|
||||
@ -45,8 +44,16 @@ def render_instructions(step): # TODO deduplicate markdown cleanup code
|
||||
except TemplateSyntaxError:
|
||||
instructions = step.instruction
|
||||
|
||||
tags = markdown_tags + ['pre', 'table', 'td', 'tr', 'th', 'tbody', 'style', 'thead']
|
||||
parsed_md = md.markdown(instructions, extensions=['markdown.extensions.fenced_code', 'tables', UrlizeExtension(), MarkdownFormatExtension()])
|
||||
tags = markdown_tags + [
|
||||
'pre', 'table', 'td', 'tr', 'th', 'tbody', 'style', 'thead'
|
||||
]
|
||||
parsed_md = md.markdown(
|
||||
instructions,
|
||||
extensions=[
|
||||
'markdown.extensions.fenced_code', 'tables',
|
||||
UrlizeExtension(), MarkdownFormatExtension()
|
||||
]
|
||||
)
|
||||
markdown_attrs['*'] = markdown_attrs['*'] + ['class']
|
||||
|
||||
return bleach.clean(parsed_md, tags, markdown_attrs)
|
||||
|
@ -389,7 +389,7 @@ msgstr "Modifica di massa"
|
||||
|
||||
#: cookbook/templates/base.html:98
|
||||
msgid "Storage Data"
|
||||
msgstr "Dati di archiviazione"
|
||||
msgstr "Dati e Archiviazione"
|
||||
|
||||
#: cookbook/templates/base.html:102
|
||||
msgid "Storage Backends"
|
||||
@ -470,8 +470,8 @@ msgstr "Modifica di massa per ricette"
|
||||
#: cookbook/templates/batch/edit.html:20
|
||||
msgid "Add the specified keywords to all recipes containing a word"
|
||||
msgstr ""
|
||||
"Aggiungi le parole chiave specificate a tutte le ricette che contengono una "
|
||||
"parola"
|
||||
"Aggiungi a tutte le ricette che contengono una determinata stringa le parole"
|
||||
" chiave desiderate "
|
||||
|
||||
#: cookbook/templates/batch/monitor.html:6 cookbook/views/edit.py:59
|
||||
msgid "Sync"
|
||||
@ -998,7 +998,7 @@ msgstr "Nuova Ricetta"
|
||||
|
||||
#: cookbook/templates/index.html:47
|
||||
msgid "Website Import"
|
||||
msgstr "Importa da Sito Web"
|
||||
msgstr "Importa dal web"
|
||||
|
||||
#: cookbook/templates/index.html:53
|
||||
msgid "Advanced Search"
|
||||
@ -1419,7 +1419,7 @@ msgstr "Commenti"
|
||||
#: cookbook/templates/recipe_view.html:469 cookbook/views/delete.py:108
|
||||
#: cookbook/views/edit.py:143
|
||||
msgid "Comment"
|
||||
msgstr "Commenta"
|
||||
msgstr "Commento"
|
||||
|
||||
#: cookbook/templates/recipes_table.html:46
|
||||
#: cookbook/templates/url_import.html:55
|
||||
@ -1711,7 +1711,7 @@ msgstr "Importa da URL"
|
||||
|
||||
#: cookbook/templates/url_import.html:23
|
||||
msgid "Enter website URL"
|
||||
msgstr "Inserisci la URL del sito web"
|
||||
msgstr "Inserisci l'indirizzo del sito web"
|
||||
|
||||
#: cookbook/templates/url_import.html:44
|
||||
msgid "Recipe Name"
|
||||
|
31
cookbook/migrations/0096_auto_20210109_2044.py
Normal file
31
cookbook/migrations/0096_auto_20210109_2044.py
Normal file
@ -0,0 +1,31 @@
|
||||
# Generated by Django 3.1.5 on 2021-01-09 19:44
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def delete_duplicate_bookmarks(apps, schema_editor):
|
||||
"""
|
||||
In this migration, a unique constraint is set on the fields `recipe` and `book`.
|
||||
If there are already duplicate entries, the migration will fail.
|
||||
Therefore all duplicate entries are deleted beforehand.
|
||||
"""
|
||||
RecipeBookEntry = apps.get_model('cookbook', 'RecipeBookEntry')
|
||||
|
||||
for row in RecipeBookEntry.objects.all():
|
||||
if RecipeBookEntry.objects.filter(recipe=row.recipe, book=row.book).count() > 1:
|
||||
row.delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('cookbook', '0095_auto_20210107_1804'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# run function to delete duplicated bookmarks
|
||||
migrations.RunPython(delete_duplicate_bookmarks),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='recipebookentry',
|
||||
unique_together={('recipe', 'book')},
|
||||
),
|
||||
]
|
@ -4,13 +4,14 @@ from datetime import date, timedelta
|
||||
|
||||
from annoying.fields import AutoOneToOneField
|
||||
from django.contrib import auth
|
||||
from django.contrib.auth.models import User, Group
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.core.validators import MinLengthValidator
|
||||
from django.utils.translation import gettext as _
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext as _
|
||||
from django_random_queryset import RandomManager
|
||||
|
||||
from recipes.settings import COMMENT_PREF_DEFAULT, FRACTION_PREF_DEFAULT, STICKY_NAV_PREF_DEFAULT
|
||||
from recipes.settings import (COMMENT_PREF_DEFAULT, FRACTION_PREF_DEFAULT,
|
||||
STICKY_NAV_PREF_DEFAULT)
|
||||
|
||||
|
||||
def get_user_name(self):
|
||||
@ -39,7 +40,12 @@ class UserPreference(models.Model):
|
||||
FLATLY = 'FLATLY'
|
||||
SUPERHERO = 'SUPERHERO'
|
||||
|
||||
THEMES = ((BOOTSTRAP, 'Bootstrap'), (DARKLY, 'Darkly'), (FLATLY, 'Flatly'), (SUPERHERO, 'Superhero'))
|
||||
THEMES = (
|
||||
(BOOTSTRAP, 'Bootstrap'),
|
||||
(DARKLY, 'Darkly'),
|
||||
(FLATLY, 'Flatly'),
|
||||
(SUPERHERO, 'Superhero')
|
||||
)
|
||||
|
||||
# Nav colors
|
||||
PRIMARY = 'PRIMARY'
|
||||
@ -51,14 +57,26 @@ class UserPreference(models.Model):
|
||||
LIGHT = 'LIGHT'
|
||||
DARK = 'DARK'
|
||||
|
||||
COLORS = ((PRIMARY, 'Primary'), (SECONDARY, 'Secondary'), (SUCCESS, 'Success'), (INFO, 'Info'), (WARNING, 'Warning'), (DANGER, 'Danger'), (LIGHT, 'Light'), (DARK, 'Dark'))
|
||||
COLORS = (
|
||||
(PRIMARY, 'Primary'),
|
||||
(SECONDARY, 'Secondary'),
|
||||
(SUCCESS, 'Success'), (INFO, 'Info'),
|
||||
(WARNING, 'Warning'),
|
||||
(DANGER, 'Danger'),
|
||||
(LIGHT, 'Light'),
|
||||
(DARK, 'Dark')
|
||||
)
|
||||
|
||||
# Default Page
|
||||
SEARCH = 'SEARCH'
|
||||
PLAN = 'PLAN'
|
||||
BOOKS = 'BOOKS'
|
||||
|
||||
PAGES = ((SEARCH, _('Search')), (PLAN, _('Meal-Plan')), (BOOKS, _('Books')),)
|
||||
PAGES = (
|
||||
(SEARCH, _('Search')),
|
||||
(PLAN, _('Meal-Plan')),
|
||||
(BOOKS, _('Books')),
|
||||
)
|
||||
|
||||
# Search Style
|
||||
SMALL = 'SMALL'
|
||||
@ -68,13 +86,21 @@ class UserPreference(models.Model):
|
||||
|
||||
user = AutoOneToOneField(User, on_delete=models.CASCADE, primary_key=True)
|
||||
theme = models.CharField(choices=THEMES, max_length=128, default=FLATLY)
|
||||
nav_color = models.CharField(choices=COLORS, max_length=128, default=PRIMARY)
|
||||
nav_color = models.CharField(
|
||||
choices=COLORS, max_length=128, default=PRIMARY
|
||||
)
|
||||
default_unit = models.CharField(max_length=32, default='g')
|
||||
use_fractions = models.BooleanField(default=FRACTION_PREF_DEFAULT)
|
||||
default_page = models.CharField(choices=PAGES, max_length=64, default=SEARCH)
|
||||
search_style = models.CharField(choices=SEARCH_STYLE, max_length=64, default=LARGE)
|
||||
default_page = models.CharField(
|
||||
choices=PAGES, max_length=64, default=SEARCH
|
||||
)
|
||||
search_style = models.CharField(
|
||||
choices=SEARCH_STYLE, max_length=64, default=LARGE
|
||||
)
|
||||
show_recent = models.BooleanField(default=True)
|
||||
plan_share = models.ManyToManyField(User, blank=True, related_name='plan_share_default')
|
||||
plan_share = models.ManyToManyField(
|
||||
User, blank=True, related_name='plan_share_default'
|
||||
)
|
||||
ingredient_decimals = models.IntegerField(default=2)
|
||||
comments = models.BooleanField(default=COMMENT_PREF_DEFAULT)
|
||||
shopping_auto_sync = models.IntegerField(default=5)
|
||||
@ -90,7 +116,9 @@ class Storage(models.Model):
|
||||
STORAGE_TYPES = ((DROPBOX, 'Dropbox'), (NEXTCLOUD, 'Nextcloud'))
|
||||
|
||||
name = models.CharField(max_length=128)
|
||||
method = models.CharField(choices=STORAGE_TYPES, max_length=128, default=DROPBOX)
|
||||
method = models.CharField(
|
||||
choices=STORAGE_TYPES, max_length=128, default=DROPBOX
|
||||
)
|
||||
username = models.CharField(max_length=128, blank=True, null=True)
|
||||
password = models.CharField(max_length=128, blank=True, null=True)
|
||||
token = models.CharField(max_length=512, blank=True, null=True)
|
||||
@ -138,7 +166,9 @@ class Keyword(models.Model):
|
||||
|
||||
|
||||
class Unit(models.Model):
|
||||
name = models.CharField(unique=True, max_length=128, validators=[MinLengthValidator(1)])
|
||||
name = models.CharField(
|
||||
unique=True, max_length=128, validators=[MinLengthValidator(1)]
|
||||
)
|
||||
description = models.TextField(blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
@ -146,16 +176,24 @@ class Unit(models.Model):
|
||||
|
||||
|
||||
class Food(models.Model):
|
||||
name = models.CharField(unique=True, max_length=128, validators=[MinLengthValidator(1)])
|
||||
recipe = models.ForeignKey('Recipe', null=True, blank=True, on_delete=models.SET_NULL)
|
||||
name = models.CharField(
|
||||
unique=True, max_length=128, validators=[MinLengthValidator(1)]
|
||||
)
|
||||
recipe = models.ForeignKey(
|
||||
'Recipe', null=True, blank=True, on_delete=models.SET_NULL
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.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)
|
||||
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)
|
||||
@ -174,7 +212,11 @@ class Step(models.Model):
|
||||
TIME = 'TIME'
|
||||
|
||||
name = models.CharField(max_length=128, default='', blank=True)
|
||||
type = models.CharField(choices=((TEXT, _('Text')), (TIME, _('Time')),), default=TEXT, max_length=16)
|
||||
type = models.CharField(
|
||||
choices=((TEXT, _('Text')), (TIME, _('Time')),),
|
||||
default=TEXT,
|
||||
max_length=16
|
||||
)
|
||||
instruction = models.TextField(blank=True)
|
||||
ingredients = models.ManyToManyField(Ingredient, blank=True)
|
||||
time = models.IntegerField(default=0, blank=True)
|
||||
@ -191,20 +233,26 @@ class Step(models.Model):
|
||||
|
||||
class NutritionInformation(models.Model):
|
||||
fats = models.DecimalField(default=0, decimal_places=16, max_digits=32)
|
||||
carbohydrates = models.DecimalField(default=0, decimal_places=16, max_digits=32)
|
||||
carbohydrates = models.DecimalField(
|
||||
default=0, decimal_places=16, max_digits=32
|
||||
)
|
||||
proteins = models.DecimalField(default=0, decimal_places=16, max_digits=32)
|
||||
calories = models.DecimalField(default=0, decimal_places=16, max_digits=32)
|
||||
source = models.CharField(max_length=512, default="", null=True, blank=True)
|
||||
source = models.CharField(
|
||||
max_length=512, default="", null=True, blank=True
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f'Nutrition'
|
||||
return 'Nutrition'
|
||||
|
||||
|
||||
class Recipe(models.Model):
|
||||
name = models.CharField(max_length=128)
|
||||
servings = models.IntegerField(default=1)
|
||||
image = models.ImageField(upload_to='recipes/', blank=True, null=True)
|
||||
storage = models.ForeignKey(Storage, on_delete=models.PROTECT, blank=True, null=True)
|
||||
storage = models.ForeignKey(
|
||||
Storage, on_delete=models.PROTECT, blank=True, null=True
|
||||
)
|
||||
file_uid = models.CharField(max_length=256, default="", blank=True)
|
||||
file_path = models.CharField(max_length=512, default="", blank=True)
|
||||
link = models.CharField(max_length=512, null=True, blank=True)
|
||||
@ -214,7 +262,9 @@ class Recipe(models.Model):
|
||||
working_time = models.IntegerField(default=0)
|
||||
waiting_time = models.IntegerField(default=0)
|
||||
internal = models.BooleanField(default=False)
|
||||
nutrition = models.ForeignKey(NutritionInformation, blank=True, null=True, on_delete=models.CASCADE)
|
||||
nutrition = models.ForeignKey(
|
||||
NutritionInformation, blank=True, null=True, on_delete=models.CASCADE
|
||||
)
|
||||
created_by = models.ForeignKey(User, on_delete=models.PROTECT)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
@ -251,7 +301,9 @@ class RecipeBook(models.Model):
|
||||
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)
|
||||
|
||||
def __str__(self):
|
||||
@ -265,6 +317,9 @@ class RecipeBookEntry(models.Model):
|
||||
def __str__(self):
|
||||
return self.recipe.name
|
||||
|
||||
class Meta:
|
||||
unique_together = (('recipe', 'book'),)
|
||||
|
||||
|
||||
class MealType(models.Model):
|
||||
name = models.CharField(max_length=128)
|
||||
@ -276,11 +331,15 @@ class MealType(models.Model):
|
||||
|
||||
|
||||
class MealPlan(models.Model):
|
||||
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, blank=True, null=True)
|
||||
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()
|
||||
@ -298,7 +357,9 @@ class MealPlan(models.Model):
|
||||
|
||||
|
||||
class ShoppingListRecipe(models.Model):
|
||||
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, null=True, blank=True)
|
||||
recipe = models.ForeignKey(
|
||||
Recipe, on_delete=models.CASCADE, null=True, blank=True
|
||||
)
|
||||
servings = models.DecimalField(default=1, max_digits=8, decimal_places=4)
|
||||
|
||||
def __str__(self):
|
||||
@ -312,9 +373,13 @@ class ShoppingListRecipe(models.Model):
|
||||
|
||||
|
||||
class ShoppingListEntry(models.Model):
|
||||
list_recipe = models.ForeignKey(ShoppingListRecipe, on_delete=models.CASCADE, null=True, blank=True)
|
||||
list_recipe = models.ForeignKey(
|
||||
ShoppingListRecipe, on_delete=models.CASCADE, null=True, blank=True
|
||||
)
|
||||
food = models.ForeignKey(Food, on_delete=models.CASCADE)
|
||||
unit = models.ForeignKey(Unit, on_delete=models.CASCADE, null=True, blank=True)
|
||||
unit = models.ForeignKey(
|
||||
Unit, on_delete=models.CASCADE, null=True, blank=True
|
||||
)
|
||||
amount = models.DecimalField(default=0, decimal_places=16, max_digits=32)
|
||||
order = models.IntegerField(default=0)
|
||||
checked = models.BooleanField(default=False)
|
||||
@ -334,7 +399,9 @@ class ShoppingList(models.Model):
|
||||
note = models.TextField(blank=True, null=True)
|
||||
recipes = models.ManyToManyField(ShoppingListRecipe, blank=True)
|
||||
entries = models.ManyToManyField(ShoppingListEntry, blank=True)
|
||||
shared = models.ManyToManyField(User, blank=True, related_name='list_share')
|
||||
shared = models.ManyToManyField(
|
||||
User, blank=True, related_name='list_share'
|
||||
)
|
||||
finished = models.BooleanField(default=False)
|
||||
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
@ -358,7 +425,9 @@ class InviteLink(models.Model):
|
||||
username = models.CharField(blank=True, max_length=64)
|
||||
group = models.ForeignKey(Group, on_delete=models.CASCADE)
|
||||
valid_until = models.DateField(default=date.today() + timedelta(days=14))
|
||||
used_by = models.ForeignKey(User, null=True, on_delete=models.CASCADE, related_name='used_by')
|
||||
used_by = models.ForeignKey(
|
||||
User, null=True, on_delete=models.CASCADE, related_name='used_by'
|
||||
)
|
||||
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
@ -1,11 +1,9 @@
|
||||
import base64
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
import requests
|
||||
import json
|
||||
|
||||
from cookbook.models import Recipe, RecipeImport, SyncLog
|
||||
from cookbook.provider.provider import Provider
|
||||
|
||||
@ -34,16 +32,26 @@ class Dropbox(Provider):
|
||||
return r
|
||||
|
||||
import_count = 0
|
||||
for recipe in recipes['entries']: # TODO check if has_more is set and import that as well
|
||||
# TODO check if has_more is set and import that as well
|
||||
for recipe in recipes['entries']:
|
||||
path = recipe['path_lower']
|
||||
if not Recipe.objects.filter(file_path__iexact=path).exists() and not RecipeImport.objects.filter(
|
||||
file_path=path).exists():
|
||||
if not Recipe.objects.filter(file_path__iexact=path).exists() \
|
||||
and not RecipeImport.objects.filter(file_path=path).exists(): # noqa: E501
|
||||
name = os.path.splitext(recipe['name'])[0]
|
||||
new_recipe = RecipeImport(name=name, file_path=path, storage=monitor.storage, file_uid=recipe['id'])
|
||||
new_recipe = RecipeImport(
|
||||
name=name,
|
||||
file_path=path,
|
||||
storage=monitor.storage,
|
||||
file_uid=recipe['id']
|
||||
)
|
||||
new_recipe.save()
|
||||
import_count += 1
|
||||
|
||||
log_entry = SyncLog(status='SUCCESS', msg='Imported ' + str(import_count) + ' recipes', sync=monitor)
|
||||
log_entry = SyncLog(
|
||||
status='SUCCESS',
|
||||
msg='Imported ' + str(import_count) + ' recipes',
|
||||
sync=monitor
|
||||
)
|
||||
log_entry.save()
|
||||
|
||||
monitor.last_checked = datetime.now()
|
||||
@ -53,7 +61,7 @@ class Dropbox(Provider):
|
||||
|
||||
@staticmethod
|
||||
def create_share_link(recipe):
|
||||
url = "https://api.dropboxapi.com/2/sharing/create_shared_link_with_settings"
|
||||
url = "https://api.dropboxapi.com/2/sharing/create_shared_link_with_settings" # noqa: E501
|
||||
|
||||
headers = {
|
||||
"Authorization": "Bearer " + recipe.storage.token,
|
||||
@ -84,8 +92,8 @@ class Dropbox(Provider):
|
||||
r = requests.post(url, headers=headers, data=json.dumps(data))
|
||||
p = r.json()
|
||||
|
||||
for l in p['links']:
|
||||
return l['url']
|
||||
for link in p['links']:
|
||||
return link['url']
|
||||
|
||||
response = Dropbox.create_share_link(recipe)
|
||||
return response['url']
|
||||
@ -96,7 +104,9 @@ 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)
|
||||
|
||||
@ -111,7 +121,11 @@ class Dropbox(Provider):
|
||||
|
||||
data = {
|
||||
"from_path": recipe.file_path,
|
||||
"to_path": os.path.dirname(recipe.file_path) + '/' + new_name + os.path.splitext(recipe.file_path)[1]
|
||||
"to_path": "%s/%s%s" % (
|
||||
os.path.dirname(recipe.file_path),
|
||||
new_name,
|
||||
os.path.splitext(recipe.file_path)[1]
|
||||
)
|
||||
}
|
||||
|
||||
r = requests.post(url, headers=headers, data=json.dumps(data))
|
||||
|
@ -1,15 +1,13 @@
|
||||
import base64
|
||||
import io
|
||||
import os
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
import webdav3.client as wc
|
||||
import requests
|
||||
from io import BytesIO
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
import requests
|
||||
import webdav3.client as wc
|
||||
from cookbook.models import Recipe, RecipeImport, SyncLog
|
||||
from cookbook.provider.provider import Provider
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
|
||||
class Nextcloud(Provider):
|
||||
@ -34,14 +32,22 @@ 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():
|
||||
if not Recipe.objects.filter(file_path__iexact=path).exists() \
|
||||
and not RecipeImport.objects.filter(file_path=path).exists(): # noqa: E501
|
||||
name = os.path.splitext(file)[0]
|
||||
new_recipe = RecipeImport(name=name, file_path=path, storage=monitor.storage)
|
||||
new_recipe = RecipeImport(
|
||||
name=name,
|
||||
file_path=path,
|
||||
storage=monitor.storage
|
||||
)
|
||||
new_recipe.save()
|
||||
import_count += 1
|
||||
|
||||
log_entry = SyncLog(status='SUCCESS', msg='Imported ' + str(import_count) + ' recipes', sync=monitor)
|
||||
log_entry = SyncLog(
|
||||
status='SUCCESS',
|
||||
msg='Imported ' + str(import_count) + ' recipes',
|
||||
sync=monitor
|
||||
)
|
||||
log_entry.save()
|
||||
|
||||
monitor.last_checked = datetime.now()
|
||||
@ -51,7 +57,7 @@ class Nextcloud(Provider):
|
||||
|
||||
@staticmethod
|
||||
def create_share_link(recipe):
|
||||
url = recipe.storage.url + '/ocs/v2.php/apps/files_sharing/api/v1/shares?format=json'
|
||||
url = recipe.storage.url + '/ocs/v2.php/apps/files_sharing/api/v1/shares?format=json' # noqa: E501
|
||||
|
||||
headers = {
|
||||
"OCS-APIRequest": "true",
|
||||
@ -60,8 +66,14 @@ 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()
|
||||
|
||||
@ -69,14 +81,20 @@ class Nextcloud(Provider):
|
||||
|
||||
@staticmethod
|
||||
def get_share_link(recipe):
|
||||
url = recipe.storage.url + '/ocs/v2.php/apps/files_sharing/api/v1/shares?format=json&path=' + recipe.file_path
|
||||
url = recipe.storage.url + '/ocs/v2.php/apps/files_sharing/api/v1/shares?format=json&path=' + recipe.file_path # noqa: E501
|
||||
|
||||
headers = {
|
||||
"OCS-APIRequest": "true",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
r = requests.get(url, headers=headers, auth=HTTPBasicAuth(recipe.storage.username, recipe.storage.password))
|
||||
r = requests.get(
|
||||
url,
|
||||
headers=headers,
|
||||
auth=HTTPBasicAuth(
|
||||
recipe.storage.username, recipe.storage.password
|
||||
)
|
||||
)
|
||||
|
||||
response_json = r.json()
|
||||
for element in response_json['ocs']['data']:
|
||||
@ -91,7 +109,10 @@ class Nextcloud(Provider):
|
||||
|
||||
tmp_file_path = tempfile.gettempdir() + '/' + recipe.name + '.pdf'
|
||||
|
||||
client.download_file(remote_path=recipe.file_path, local_path=tmp_file_path)
|
||||
client.download_file(
|
||||
remote_path=recipe.file_path,
|
||||
local_path=tmp_file_path
|
||||
)
|
||||
|
||||
file = io.BytesIO(open(tmp_file_path, 'rb').read())
|
||||
os.remove(tmp_file_path)
|
||||
@ -102,8 +123,14 @@ class Nextcloud(Provider):
|
||||
def rename_file(recipe, new_name):
|
||||
client = Nextcloud.get_client(recipe.storage)
|
||||
|
||||
client.move(recipe.file_path,
|
||||
os.path.dirname(recipe.file_path) + '/' + new_name + os.path.splitext(recipe.file_path)[1])
|
||||
client.move(
|
||||
recipe.file_path,
|
||||
"%s/%s%s" % (
|
||||
os.path.dirname(recipe.file_path),
|
||||
new_name,
|
||||
os.path.splitext(recipe.file_path)[1]
|
||||
)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
@ -1,18 +1,24 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from drf_writable_nested import WritableNestedModelSerializer, UniqueFieldsMixin
|
||||
from drf_writable_nested import (UniqueFieldsMixin,
|
||||
WritableNestedModelSerializer)
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from cookbook.models import MealPlan, MealType, Recipe, ViewLog, UserPreference, Storage, Sync, SyncLog, Keyword, Unit, Ingredient, Comment, RecipeImport, RecipeBook, RecipeBookEntry, ShareLink, CookLog, Food, Step, ShoppingList, \
|
||||
ShoppingListEntry, ShoppingListRecipe, NutritionInformation
|
||||
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)
|
||||
from cookbook.templatetags.custom_tags import markdown
|
||||
|
||||
|
||||
class CustomDecimalField(serializers.Field):
|
||||
"""
|
||||
Custom decimal field to normalize useless decimal places and allow commas as decimal separators
|
||||
Custom decimal field to normalize useless decimal places
|
||||
and allow commas as decimal separators
|
||||
"""
|
||||
|
||||
def to_representation(self, value):
|
||||
@ -47,15 +53,21 @@ class UserNameSerializer(WritableNestedModelSerializer):
|
||||
class UserPreferenceSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = UserPreference
|
||||
fields = ('user', 'theme', 'nav_color', 'default_unit', 'default_page', 'search_style', 'show_recent',
|
||||
'plan_share', 'ingredient_decimals', 'comments')
|
||||
fields = (
|
||||
'user', 'theme', 'nav_color', 'default_unit', 'default_page',
|
||||
'search_style', 'show_recent', 'plan_share', 'ingredient_decimals',
|
||||
'comments'
|
||||
)
|
||||
read_only_fields = ['user']
|
||||
|
||||
|
||||
class StorageSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Storage
|
||||
fields = ('id', 'name', 'method', 'username', 'password', 'token', 'created_by')
|
||||
fields = (
|
||||
'id', 'name', 'method', 'username', 'password',
|
||||
'token', 'created_by'
|
||||
)
|
||||
|
||||
extra_kwargs = {
|
||||
'password': {'write_only': True},
|
||||
@ -66,7 +78,10 @@ class StorageSerializer(serializers.ModelSerializer):
|
||||
class SyncSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Sync
|
||||
fields = ('id', 'storage', 'path', 'active', 'last_checked', 'created_at', 'updated_at')
|
||||
fields = (
|
||||
'id', 'storage', 'path', 'active', 'last_checked',
|
||||
'created_at', 'updated_at'
|
||||
)
|
||||
|
||||
|
||||
class SyncLogSerializer(serializers.ModelSerializer):
|
||||
@ -82,13 +97,17 @@ 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
|
||||
# since multi select tags dont have id's
|
||||
# duplicate names might be routed to create
|
||||
obj, created = Keyword.objects.get_or_create(**validated_data)
|
||||
return obj
|
||||
|
||||
class Meta:
|
||||
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',)
|
||||
|
||||
@ -96,7 +115,8 @@ 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
|
||||
# since multi select tags dont have id's
|
||||
# duplicate names might be routed to create
|
||||
obj, created = Unit.objects.get_or_create(**validated_data)
|
||||
return obj
|
||||
|
||||
@ -109,7 +129,8 @@ class UnitSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
|
||||
class FoodSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
|
||||
|
||||
def create(self, validated_data):
|
||||
# since multi select tags dont have id's duplicate names might be routed to create
|
||||
# since multi select tags dont have id's
|
||||
# duplicate names might be routed to create
|
||||
obj, created = Food.objects.get_or_create(**validated_data)
|
||||
return obj
|
||||
|
||||
@ -129,7 +150,10 @@ class IngredientSerializer(WritableNestedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Ingredient
|
||||
fields = ('id', 'food', 'unit', 'amount', 'note', 'order', 'is_header', 'no_amount')
|
||||
fields = (
|
||||
'id', 'food', 'unit', 'amount', 'note', 'order',
|
||||
'is_header', 'no_amount'
|
||||
)
|
||||
|
||||
|
||||
class StepSerializer(WritableNestedModelSerializer):
|
||||
@ -137,7 +161,10 @@ class StepSerializer(WritableNestedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Step
|
||||
fields = ('id', 'name', 'type', 'instruction', 'ingredients', 'time', 'order', 'show_as_header')
|
||||
fields = (
|
||||
'id', 'name', 'type', 'instruction', 'ingredients',
|
||||
'time', 'order', 'show_as_header'
|
||||
)
|
||||
|
||||
|
||||
class NutritionInformationSerializer(serializers.ModelSerializer):
|
||||
@ -153,7 +180,11 @@ class RecipeSerializer(WritableNestedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Recipe
|
||||
fields = ['id', 'name', 'image', 'keywords', 'steps', 'working_time', 'waiting_time', 'created_by', 'created_at', 'updated_at', 'internal', 'nutrition', 'servings']
|
||||
fields = (
|
||||
'id', 'name', 'image', 'keywords', 'steps', 'working_time',
|
||||
'waiting_time', 'created_by', 'created_at', 'updated_at',
|
||||
'internal', 'nutrition', 'servings'
|
||||
)
|
||||
read_only_fields = ['image', 'created_by', 'created_at']
|
||||
|
||||
def create(self, validated_data):
|
||||
@ -209,7 +240,11 @@ class MealPlanSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = MealPlan
|
||||
fields = ('id', 'title', 'recipe', 'servings', 'note', 'note_markdown', 'date', 'meal_type', 'created_by', 'shared', 'recipe_name', 'meal_type_name')
|
||||
fields = (
|
||||
'id', 'title', 'recipe', 'servings', 'note', 'note_markdown',
|
||||
'date', 'meal_type', 'created_by', 'shared', 'recipe_name',
|
||||
'meal_type_name'
|
||||
)
|
||||
|
||||
|
||||
class ShoppingListRecipeSerializer(serializers.ModelSerializer):
|
||||
@ -229,7 +264,9 @@ class ShoppingListEntrySerializer(WritableNestedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ShoppingListEntry
|
||||
fields = ('id', 'list_recipe', 'food', 'unit', 'amount', 'order', 'checked')
|
||||
fields = (
|
||||
'id', 'list_recipe', 'food', 'unit', 'amount', 'order', 'checked'
|
||||
)
|
||||
read_only_fields = ('id',)
|
||||
|
||||
|
||||
@ -246,7 +283,10 @@ class ShoppingListSerializer(WritableNestedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ShoppingList
|
||||
fields = ('id', 'uuid', 'note', 'recipes', 'entries', 'shared', 'finished', 'created_by', 'created_at',)
|
||||
fields = (
|
||||
'id', 'uuid', 'note', 'recipes', 'entries',
|
||||
'shared', 'finished', 'created_by', 'created_at'
|
||||
)
|
||||
read_only_fields = ('id',)
|
||||
|
||||
|
||||
|
BIN
cookbook/static/manifest/icon-192.png
Normal file
BIN
cookbook/static/manifest/icon-192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.0 KiB |
BIN
cookbook/static/manifest/icon-512.png
Normal file
BIN
cookbook/static/manifest/icon-512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.4 KiB |
44
cookbook/static/manifest/webmanifest
Normal file
44
cookbook/static/manifest/webmanifest
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "Recipes",
|
||||
"description": "Application to manage, tag and search recipes.",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/manifest/icon-192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "/static/manifest/icon-512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": "/search",
|
||||
"background_color": "#18BC9C",
|
||||
"display": "standalone",
|
||||
"scope": "/",
|
||||
"theme_color": "#18BC9C",
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Plan",
|
||||
"short_name": "Plan",
|
||||
"description": "View your meal Plan",
|
||||
"url": "/plan",
|
||||
"icons": [{ "src": "/static/manifest/icon-192.png", "sizes": "192x192" }]
|
||||
},
|
||||
{
|
||||
"name": "Books",
|
||||
"short_name": "Cookbooks",
|
||||
"description": "View your cookbooks",
|
||||
"url": "/books",
|
||||
"icons": [{ "src": "/static/manifest/icon-192.png", "sizes": "192x192" }]
|
||||
},
|
||||
{
|
||||
"name": "Shopping",
|
||||
"short_name": "Shopping List",
|
||||
"description": "View your shopping lists",
|
||||
"url": "/list/shopping-list/",
|
||||
"icons": [{ "src": "/static/manifest/icon-192.png", "sizes": "192x192" }]
|
||||
}
|
||||
]
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
import django_tables2 as tables
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext as _
|
||||
from django_tables2.utils import A # alias for Accessor
|
||||
from django_tables2.utils import A
|
||||
|
||||
from .models import *
|
||||
from .models import (CookLog, InviteLink, Keyword, Recipe, RecipeImport,
|
||||
ShoppingList, Storage, Sync, SyncLog, ViewLog)
|
||||
|
||||
|
||||
class ImageUrlColumn(tables.Column):
|
||||
@ -17,7 +18,11 @@ class RecipeTableSmall(tables.Table):
|
||||
id = tables.LinkColumn('edit_recipe', args=[A('id')])
|
||||
name = tables.LinkColumn('view_recipe', args=[A('id')])
|
||||
all_tags = tables.Column(
|
||||
attrs={'td': {'class': 'd-none d-lg-table-cell'}, 'th': {'class': 'd-none d-lg-table-cell'}})
|
||||
attrs={
|
||||
'td': {'class': 'd-none d-lg-table-cell'},
|
||||
'th': {'class': 'd-none d-lg-table-cell'}
|
||||
}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Recipe
|
||||
@ -26,16 +31,25 @@ class RecipeTableSmall(tables.Table):
|
||||
|
||||
|
||||
class RecipeTable(tables.Table):
|
||||
edit = tables.TemplateColumn("<a style='color: inherit' href='{% url 'edit_recipe' record.id %}' >" + _('Edit') + "</a>")
|
||||
edit = tables.TemplateColumn(
|
||||
"<a style='color: inherit' href='{% url 'edit_recipe' record.id %}' >" + _('Edit') + "</a>" # noqa: E501
|
||||
)
|
||||
name = tables.LinkColumn('view_recipe', args=[A('id')])
|
||||
all_tags = tables.Column(
|
||||
attrs={'td': {'class': 'd-none d-lg-table-cell'}, 'th': {'class': 'd-none d-lg-table-cell'}})
|
||||
attrs={
|
||||
'td': {'class': 'd-none d-lg-table-cell'},
|
||||
'th': {'class': 'd-none d-lg-table-cell'}
|
||||
}
|
||||
)
|
||||
image = ImageUrlColumn()
|
||||
|
||||
class Meta:
|
||||
model = Recipe
|
||||
template_name = 'recipes_table.html'
|
||||
fields = ('id', 'name', 'all_tags', 'image', 'instructions', 'working_time', 'waiting_time', 'internal')
|
||||
fields = (
|
||||
'id', 'name', 'all_tags', 'image', 'instructions',
|
||||
'working_time', 'waiting_time', 'internal'
|
||||
)
|
||||
|
||||
|
||||
class KeywordTable(tables.Table):
|
||||
@ -71,9 +85,13 @@ class ImportLogTable(tables.Table):
|
||||
@staticmethod
|
||||
def render_status(value):
|
||||
if value == 'SUCCESS':
|
||||
return format_html('<span class="badge badge-success">%s</span>' % value)
|
||||
return format_html(
|
||||
'<span class="badge badge-success">%s</span>' % value
|
||||
)
|
||||
else:
|
||||
return format_html('<span class="badge badge-danger">%s</span>' % value)
|
||||
return format_html(
|
||||
'<span class="badge badge-danger">%s</span>' % value
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SyncLog
|
||||
@ -90,7 +108,9 @@ class SyncTable(tables.Table):
|
||||
|
||||
@staticmethod
|
||||
def render_storage(value):
|
||||
return format_html('<span class="badge badge-success">%s</span>' % value)
|
||||
return format_html(
|
||||
'<span class="badge badge-success">%s</span>' % value
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Sync
|
||||
@ -100,7 +120,9 @@ class SyncTable(tables.Table):
|
||||
|
||||
class RecipeImportTable(tables.Table):
|
||||
id = tables.LinkColumn('new_recipe_import', args=[A('id')])
|
||||
delete = tables.TemplateColumn("<a href='{% url 'delete_recipe_import' record.id %}' >" + _('Delete') + "</a>")
|
||||
delete = tables.TemplateColumn(
|
||||
"<a href='{% url 'delete_recipe_import' record.id %}' >" + _('Delete') + "</a>" # noqa: E501
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RecipeImport
|
||||
@ -118,13 +140,19 @@ class ShoppingListTable(tables.Table):
|
||||
|
||||
|
||||
class InviteLinkTable(tables.Table):
|
||||
link = tables.TemplateColumn("<a href='{% url 'view_signup' record.uuid %}' >" + _('Link') + "</a>")
|
||||
delete = tables.TemplateColumn("<a href='{% url 'delete_invite_link' record.id %}' >" + _('Delete') + "</a>")
|
||||
link = tables.TemplateColumn(
|
||||
"<a href='{% url 'view_signup' record.uuid %}' >" + _('Link') + "</a>"
|
||||
)
|
||||
delete = tables.TemplateColumn(
|
||||
"<a href='{% url 'delete_invite_link' record.id %}' >" + _('Delete') + "</a>" # noqa: E501
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InviteLink
|
||||
template_name = 'generic/table_template.html'
|
||||
fields = ('username', 'group', 'valid_until', 'created_by', 'created_at')
|
||||
fields = (
|
||||
'username', 'group', 'valid_until', 'created_by', 'created_at'
|
||||
)
|
||||
|
||||
|
||||
class ViewLogTable(tables.Table):
|
||||
|
@ -16,6 +16,7 @@
|
||||
<link rel="icon" type="image/png" href="{% static 'favicon.png' %}" sizes="32x32">
|
||||
<link rel="icon" type="image/png" href="{% static 'favicon.png' %}" sizes="96x96">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{% static 'favicon.png' %}">
|
||||
<link rel="manifest" href="{% static 'manifest/webmanifest' %}">
|
||||
<meta name="msapplication-TileColor" content="#ffffff">
|
||||
<meta name="msapplication-TileImage" content="/mstile-144x144.png">
|
||||
|
||||
@ -48,7 +49,8 @@
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-{% nav_color request %}" id="id_main_nav" style="{% sticky_nav request %}">
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-{% nav_color request %}" id="id_main_nav"
|
||||
style="{% sticky_nav request %}">
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarText"
|
||||
aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
@ -210,5 +212,17 @@
|
||||
{% block script %}
|
||||
{% endblock script %}
|
||||
|
||||
<script type="application/javascript">
|
||||
window.addEventListener("load", () => {
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker.register("{% url 'service_worker' %}", { scope: '/' }).then(function (reg) {
|
||||
console.log('Successfully registered service worker', reg);
|
||||
}).catch(function (err) {
|
||||
console.warn('Error whilst registering service worker', err);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
31
cookbook/templates/offline.html
Normal file
31
cookbook/templates/offline.html
Normal file
@ -0,0 +1,31 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Offline" %}{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div style="text-align: center">
|
||||
|
||||
<h1 class="">Offline</h1>
|
||||
<br/>
|
||||
|
||||
<span>{% trans 'You are currently offline!' %}</span>
|
||||
<span>{% trans 'This app does not (yet) support offline functionality. Please make sure to re-establish a network connection.' %}</span>
|
||||
<br/>
|
||||
<br/>
|
||||
<i class="fas fa-search fa-8x"></i>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
88
cookbook/templates/service-worker.js
Normal file
88
cookbook/templates/service-worker.js
Normal file
@ -0,0 +1,88 @@
|
||||
/*
|
||||
Copyright 2015, 2019, 2020 Google LLC. All Rights Reserved.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Incrementing OFFLINE_VERSION will kick off the install event and force
|
||||
// previously cached resources to be updated from the network.
|
||||
const OFFLINE_VERSION = 1;
|
||||
const CACHE_NAME = "offline";
|
||||
// Customize this with a different URL if needed.
|
||||
const OFFLINE_URL = "/offline/";
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
event.waitUntil(
|
||||
(async () => {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
// Setting {cache: 'reload'} in the new request will ensure that the
|
||||
// response isn't fulfilled from the HTTP cache; i.e., it will be from
|
||||
// the network.
|
||||
await cache.add(new Request(OFFLINE_URL, {cache: "reload"}));
|
||||
})()
|
||||
);
|
||||
// Force the waiting service worker to become the active service worker.
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(
|
||||
(async () => {
|
||||
// Enable navigation preload if it's supported.
|
||||
// See https://developers.google.com/web/updates/2017/02/navigation-preload
|
||||
if ("navigationPreload" in self.registration) {
|
||||
await self.registration.navigationPreload.enable();
|
||||
}
|
||||
})()
|
||||
);
|
||||
|
||||
// Tell the active service worker to take control of the page immediately.
|
||||
self.clients.claim();
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", (event) => {
|
||||
// We only want to call event.respondWith() if this is a navigation request
|
||||
// for an HTML page.
|
||||
console.log("fetch event called");
|
||||
if (event.request.mode === "navigate") {
|
||||
event.respondWith(
|
||||
(async () => {
|
||||
try {
|
||||
// First, try to use the navigation preload response if it's supported.
|
||||
const preloadResponse = await event.preloadResponse;
|
||||
if (preloadResponse) {
|
||||
return preloadResponse;
|
||||
}
|
||||
|
||||
// Always try the network first.
|
||||
console.log("Served from network");
|
||||
const networkResponse = await fetch(event.request);
|
||||
return networkResponse;
|
||||
} catch (error) {
|
||||
// catch is only triggered if an exception is thrown, which is likely
|
||||
// due to a network error.
|
||||
// If fetch() returns a valid HTTP response with a response code in
|
||||
// the 4xx or 5xx range, the catch() will NOT be called.
|
||||
console.log("Fetch failed; returning offline page instead.", error);
|
||||
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
const cachedResponse = await cache.match(OFFLINE_URL);
|
||||
return cachedResponse;
|
||||
}
|
||||
})()
|
||||
);
|
||||
}
|
||||
|
||||
// If our if() condition is false, then this fetch handler won't intercept the
|
||||
// request. If there are any other fetch handlers registered, they will get a
|
||||
// chance to call event.respondWith(). If no fetch handlers call
|
||||
// event.respondWith(), the request will be handled by the browser as if there
|
||||
// were no service worker involvement.
|
||||
});
|
0
cookbook/templatetags/__init__.py
Normal file
0
cookbook/templatetags/__init__.py
Normal file
@ -1,13 +1,12 @@
|
||||
import bleach
|
||||
import markdown as md
|
||||
from bleach_whitelist import markdown_tags, markdown_attrs
|
||||
from django import template
|
||||
from django.db.models import Avg
|
||||
from django.urls import reverse, NoReverseMatch
|
||||
|
||||
from bleach_whitelist import markdown_attrs, markdown_tags
|
||||
from cookbook.helper.mdx_attributes import MarkdownFormatExtension
|
||||
from cookbook.helper.mdx_urlize import UrlizeExtension
|
||||
from cookbook.models import get_model_name, Space
|
||||
from cookbook.models import Space, get_model_name
|
||||
from django import template
|
||||
from django.db.models import Avg
|
||||
from django.urls import NoReverseMatch, reverse
|
||||
from recipes import settings
|
||||
|
||||
register = template.Library()
|
||||
@ -33,8 +32,16 @@ def delete_url(model, pk):
|
||||
|
||||
@register.filter()
|
||||
def markdown(value):
|
||||
tags = markdown_tags + ['pre', 'table', 'td', 'tr', 'th', 'tbody', 'style', 'thead']
|
||||
parsed_md = md.markdown(value, extensions=['markdown.extensions.fenced_code', 'tables', UrlizeExtension(), MarkdownFormatExtension()])
|
||||
tags = markdown_tags + [
|
||||
'pre', 'table', 'td', 'tr', 'th', 'tbody', 'style', 'thead'
|
||||
]
|
||||
parsed_md = md.markdown(
|
||||
value,
|
||||
extensions=[
|
||||
'markdown.extensions.fenced_code', 'tables',
|
||||
UrlizeExtension(), MarkdownFormatExtension()
|
||||
]
|
||||
)
|
||||
markdown_attrs['*'] = markdown_attrs['*'] + ['class']
|
||||
return bleach.clean(parsed_md, tags, markdown_attrs)
|
||||
|
||||
@ -43,7 +50,9 @@ def markdown(value):
|
||||
def recipe_rating(recipe, user):
|
||||
if not user.is_authenticated:
|
||||
return ''
|
||||
rating = recipe.cooklog_set.filter(created_by=user).aggregate(Avg('rating'))
|
||||
rating = recipe.cooklog_set \
|
||||
.filter(created_by=user) \
|
||||
.aggregate(Avg('rating'))
|
||||
if rating['rating__avg']:
|
||||
|
||||
rating_stars = '<span style="display: inline-block;">'
|
||||
@ -51,7 +60,7 @@ def recipe_rating(recipe, user):
|
||||
rating_stars = rating_stars + '<i class="fas fa-star fa-xs"></i>'
|
||||
|
||||
if rating['rating__avg'] % 1 >= 0.5:
|
||||
rating_stars = rating_stars + '<i class="fas fa-star-half-alt fa-xs"></i>'
|
||||
rating_stars = rating_stars + '<i class="fas fa-star-half-alt fa-xs"></i>' # noqa: E501
|
||||
|
||||
rating_stars += '</span>'
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
from cookbook.models import UserPreference
|
||||
from django import template
|
||||
from django.templatetags.static import static
|
||||
|
||||
from cookbook.models import UserPreference
|
||||
from recipes.settings import STICKY_NAV_PREF_DEFAULT
|
||||
|
||||
register = template.Library()
|
||||
@ -33,7 +32,7 @@ def nav_color(request):
|
||||
@register.simple_tag
|
||||
def sticky_nav(request):
|
||||
if (not request.user.is_authenticated and STICKY_NAV_PREF_DEFAULT) or \
|
||||
(request.user.is_authenticated and request.user.userpreference.sticky_navbar):
|
||||
(request.user.is_authenticated and request.user.userpreference.sticky_navbar): # noqa: E501
|
||||
return 'position: sticky; top: 0; left: 0; z-index: 1000;'
|
||||
else:
|
||||
return ''
|
||||
|
@ -1,9 +1,8 @@
|
||||
import json
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
from cookbook.models import Food
|
||||
from cookbook.tests.views.test_views import TestViews
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
class TestApiUnit(TestViews):
|
||||
@ -19,8 +18,16 @@ class TestApiUnit(TestViews):
|
||||
|
||||
def test_keyword_list(self):
|
||||
# verify view permissions are applied accordingly
|
||||
self.batch_requests([(self.anonymous_client, 403), (self.guest_client_1, 403), (self.user_client_1, 200), (self.admin_client_1, 200), (self.superuser_client, 200)],
|
||||
reverse('api:food-list'))
|
||||
self.batch_requests(
|
||||
[
|
||||
(self.anonymous_client, 403),
|
||||
(self.guest_client_1, 403),
|
||||
(self.user_client_1, 200),
|
||||
(self.admin_client_1, 200),
|
||||
(self.superuser_client, 200)
|
||||
],
|
||||
reverse('api:food-list')
|
||||
)
|
||||
|
||||
# verify storage is returned
|
||||
r = self.user_client_1.get(reverse('api:food-list'))
|
||||
@ -42,12 +49,21 @@ class TestApiUnit(TestViews):
|
||||
self.assertEqual(len(response), 1)
|
||||
|
||||
def test_keyword_update(self):
|
||||
r = self.user_client_1.patch(reverse('api:food-detail', args={self.food_1.id}), {'name': 'new'}, content_type='application/json')
|
||||
r = self.user_client_1.patch(
|
||||
reverse(
|
||||
'api:food-detail',
|
||||
args={self.food_1.id}
|
||||
),
|
||||
{'name': 'new'},
|
||||
content_type='application/json'
|
||||
)
|
||||
response = json.loads(r.content)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(response['name'], 'new')
|
||||
|
||||
def test_keyword_delete(self):
|
||||
r = self.user_client_1.delete(reverse('api:food-detail', args={self.food_1.id}))
|
||||
r = self.user_client_1.delete(
|
||||
reverse('api:food-detail', args={self.food_1.id})
|
||||
)
|
||||
self.assertEqual(r.status_code, 204)
|
||||
self.assertEqual(Food.objects.count(), 1)
|
||||
|
@ -1,11 +1,8 @@
|
||||
import json
|
||||
|
||||
from django.contrib import auth
|
||||
from django.db.models import ProtectedError
|
||||
from django.urls import reverse
|
||||
|
||||
from cookbook.models import Storage, Sync, Keyword
|
||||
from cookbook.models import Keyword
|
||||
from cookbook.tests.views.test_views import TestViews
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
class TestApiKeyword(TestViews):
|
||||
@ -21,8 +18,16 @@ class TestApiKeyword(TestViews):
|
||||
|
||||
def test_keyword_list(self):
|
||||
# verify view permissions are applied accordingly
|
||||
self.batch_requests([(self.anonymous_client, 403), (self.guest_client_1, 403), (self.user_client_1, 200), (self.admin_client_1, 200), (self.superuser_client, 200)],
|
||||
reverse('api:keyword-list'))
|
||||
self.batch_requests(
|
||||
[
|
||||
(self.anonymous_client, 403),
|
||||
(self.guest_client_1, 403),
|
||||
(self.user_client_1, 200),
|
||||
(self.admin_client_1, 200),
|
||||
(self.superuser_client, 200)
|
||||
],
|
||||
reverse('api:keyword-list')
|
||||
)
|
||||
|
||||
# verify storage is returned
|
||||
r = self.user_client_1.get(reverse('api:keyword-list'))
|
||||
@ -35,7 +40,9 @@ class TestApiKeyword(TestViews):
|
||||
response = json.loads(r.content)
|
||||
self.assertEqual(len(response), 1)
|
||||
|
||||
r = self.user_client_1.get(f'{reverse("api:keyword-list")}?query=chicken')
|
||||
r = self.user_client_1.get(
|
||||
f'{reverse("api:keyword-list")}?query=chicken'
|
||||
)
|
||||
response = json.loads(r.content)
|
||||
self.assertEqual(len(response), 0)
|
||||
|
||||
@ -44,12 +51,24 @@ class TestApiKeyword(TestViews):
|
||||
self.assertEqual(len(response), 1)
|
||||
|
||||
def test_keyword_update(self):
|
||||
r = self.user_client_1.patch(reverse('api:keyword-detail', args={self.keyword_1.id}), {'name': 'new'}, content_type='application/json')
|
||||
r = self.user_client_1.patch(
|
||||
reverse(
|
||||
'api:keyword-detail',
|
||||
args={self.keyword_1.id}
|
||||
),
|
||||
{'name': 'new'},
|
||||
content_type='application/json'
|
||||
)
|
||||
response = json.loads(r.content)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(response['name'], 'new')
|
||||
|
||||
def test_keyword_delete(self):
|
||||
r = self.user_client_1.delete(reverse('api:keyword-detail', args={self.keyword_1.id}))
|
||||
r = self.user_client_1.delete(
|
||||
reverse(
|
||||
'api:keyword-detail',
|
||||
args={self.keyword_1.id}
|
||||
)
|
||||
)
|
||||
self.assertEqual(r.status_code, 204)
|
||||
self.assertEqual(Keyword.objects.count(), 1)
|
||||
|
@ -1,11 +1,7 @@
|
||||
import json
|
||||
|
||||
from django.contrib import auth
|
||||
from django.db.models import ProtectedError
|
||||
from django.urls import reverse
|
||||
|
||||
from cookbook.models import Storage, Sync, Keyword, ShoppingList, Recipe
|
||||
from cookbook.models import Recipe
|
||||
from cookbook.tests.views.test_views import TestViews
|
||||
from django.contrib import auth
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
class TestApiShopping(TestViews):
|
||||
@ -19,8 +15,17 @@ class TestApiShopping(TestViews):
|
||||
)
|
||||
|
||||
def test_shopping_view_permissions(self):
|
||||
self.batch_requests([(self.anonymous_client, 403), (self.guest_client_1, 200), (self.user_client_1, 200),
|
||||
(self.user_client_2, 200), (self.admin_client_1, 200), (self.superuser_client, 200)],
|
||||
reverse('api:recipe-detail', args={self.internal_recipe.id}))
|
||||
self.batch_requests(
|
||||
[
|
||||
(self.anonymous_client, 403),
|
||||
(self.guest_client_1, 200),
|
||||
(self.user_client_1, 200),
|
||||
(self.user_client_2, 200),
|
||||
(self.admin_client_1, 200),
|
||||
(self.superuser_client, 200)
|
||||
],
|
||||
reverse(
|
||||
'api:recipe-detail', args={self.internal_recipe.id})
|
||||
)
|
||||
|
||||
# TODO add tests for editing
|
||||
|
@ -1,27 +1,48 @@
|
||||
import json
|
||||
|
||||
from django.contrib import auth
|
||||
from django.db.models import ProtectedError
|
||||
from django.urls import reverse
|
||||
|
||||
from cookbook.models import Storage, Sync, Keyword, ShoppingList
|
||||
from cookbook.models import ShoppingList
|
||||
from cookbook.tests.views.test_views import TestViews
|
||||
from django.contrib import auth
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
class TestApiShopping(TestViews):
|
||||
|
||||
def setUp(self):
|
||||
super(TestApiShopping, self).setUp()
|
||||
self.list_1 = ShoppingList.objects.create(created_by=auth.get_user(self.user_client_1))
|
||||
self.list_2 = ShoppingList.objects.create(created_by=auth.get_user(self.user_client_2))
|
||||
self.list_1 = ShoppingList.objects.create(
|
||||
created_by=auth.get_user(self.user_client_1)
|
||||
)
|
||||
self.list_2 = ShoppingList.objects.create(
|
||||
created_by=auth.get_user(self.user_client_2)
|
||||
)
|
||||
|
||||
def test_shopping_view_permissions(self):
|
||||
self.batch_requests([(self.anonymous_client, 403), (self.guest_client_1, 404), (self.user_client_1, 200), (self.user_client_2, 404), (self.admin_client_1, 404), (self.superuser_client, 200)],
|
||||
reverse('api:shoppinglist-detail', args={self.list_1.id}))
|
||||
self.batch_requests(
|
||||
[
|
||||
(self.anonymous_client, 403),
|
||||
(self.guest_client_1, 404),
|
||||
(self.user_client_1, 200),
|
||||
(self.user_client_2, 404),
|
||||
(self.admin_client_1, 404),
|
||||
(self.superuser_client, 200)
|
||||
],
|
||||
reverse(
|
||||
'api:shoppinglist-detail', args={self.list_1.id}
|
||||
)
|
||||
)
|
||||
|
||||
self.list_1.shared.add(auth.get_user(self.user_client_2))
|
||||
|
||||
self.batch_requests([(self.anonymous_client, 403), (self.guest_client_1, 404), (self.user_client_1, 200), (self.user_client_2, 200), (self.admin_client_1, 404), (self.superuser_client, 200)],
|
||||
reverse('api:shoppinglist-detail', args={self.list_1.id}))
|
||||
self.batch_requests(
|
||||
[
|
||||
(self.anonymous_client, 403),
|
||||
(self.guest_client_1, 404),
|
||||
(self.user_client_1, 200),
|
||||
(self.user_client_2, 200),
|
||||
(self.admin_client_1, 404),
|
||||
(self.superuser_client, 200)
|
||||
],
|
||||
reverse(
|
||||
'api:shoppinglist-detail', args={self.list_1.id})
|
||||
)
|
||||
|
||||
# TODO add tests for editing
|
||||
|
@ -1,11 +1,10 @@
|
||||
import json
|
||||
|
||||
from django.contrib import auth
|
||||
from django.db.models import ProtectedError
|
||||
from django.urls import reverse
|
||||
|
||||
from cookbook.models import Storage, Sync
|
||||
from cookbook.tests.views.test_views import TestViews
|
||||
from django.contrib import auth
|
||||
from django.db.models import ProtectedError
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
class TestApiStorage(TestViews):
|
||||
@ -23,8 +22,16 @@ class TestApiStorage(TestViews):
|
||||
|
||||
def test_storage_list(self):
|
||||
# verify view permissions are applied accordingly
|
||||
self.batch_requests([(self.anonymous_client, 403), (self.guest_client_1, 403), (self.user_client_1, 403), (self.admin_client_1, 200), (self.superuser_client, 200)],
|
||||
reverse('api:storage-list'))
|
||||
self.batch_requests(
|
||||
[
|
||||
(self.anonymous_client, 403),
|
||||
(self.guest_client_1, 403),
|
||||
(self.user_client_1, 403),
|
||||
(self.admin_client_1, 200),
|
||||
(self.superuser_client, 200)
|
||||
],
|
||||
reverse('api:storage-list')
|
||||
)
|
||||
|
||||
# verify storage is returned
|
||||
r = self.admin_client_1.get(reverse('api:storage-list'))
|
||||
@ -38,7 +45,14 @@ class TestApiStorage(TestViews):
|
||||
|
||||
def test_storage_update(self):
|
||||
# can update storage as admin
|
||||
r = self.admin_client_1.patch(reverse('api:storage-detail', args={self.storage.id}), {'name': 'new', 'password': 'new_password'}, content_type='application/json')
|
||||
r = self.admin_client_1.patch(
|
||||
reverse(
|
||||
'api:storage-detail',
|
||||
args={self.storage.id}
|
||||
),
|
||||
{'name': 'new', 'password': 'new_password'},
|
||||
content_type='application/json'
|
||||
)
|
||||
response = json.loads(r.content)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(response['name'], 'new')
|
||||
@ -49,13 +63,20 @@ class TestApiStorage(TestViews):
|
||||
|
||||
def test_storage_delete(self):
|
||||
# can delete storage as admin
|
||||
r = self.admin_client_1.delete(reverse('api:storage-detail', args={self.storage.id}))
|
||||
r = self.admin_client_1.delete(
|
||||
reverse('api:storage-detail', args={self.storage.id})
|
||||
)
|
||||
self.assertEqual(r.status_code, 204)
|
||||
self.assertEqual(Storage.objects.count(), 0)
|
||||
|
||||
self.storage = Storage.objects.create(created_by=auth.get_user(self.admin_client_1), name='test protect')
|
||||
self.storage = Storage.objects.create(
|
||||
created_by=auth.get_user(self.admin_client_1), name='test protect'
|
||||
)
|
||||
Sync.objects.create(storage=self.storage, )
|
||||
|
||||
# test if deleting a storage with existing sync fails (as sync protects storage)
|
||||
# test if deleting a storage with existing
|
||||
# sync fails (as sync protects storage)
|
||||
with self.assertRaises(ProtectedError):
|
||||
self.admin_client_1.delete(reverse('api:storage-detail', args={self.storage.id}))
|
||||
self.admin_client_1.delete(
|
||||
reverse('api:storage-detail', args={self.storage.id})
|
||||
)
|
||||
|
@ -1,11 +1,9 @@
|
||||
import json
|
||||
|
||||
from django.contrib import auth
|
||||
from django.db.models import ProtectedError
|
||||
from django.urls import reverse
|
||||
|
||||
from cookbook.models import Storage, Sync, SyncLog
|
||||
from cookbook.tests.views.test_views import TestViews
|
||||
from django.contrib import auth
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
class TestApiSyncLog(TestViews):
|
||||
@ -26,12 +24,22 @@ class TestApiSyncLog(TestViews):
|
||||
path='path'
|
||||
)
|
||||
|
||||
self.sync_log = SyncLog.objects.create(sync=self.sync, status='success')
|
||||
self.sync_log = SyncLog.objects.create(
|
||||
sync=self.sync, status='success'
|
||||
)
|
||||
|
||||
def test_sync_log_list(self):
|
||||
# verify view permissions are applied accordingly
|
||||
self.batch_requests([(self.anonymous_client, 403), (self.guest_client_1, 403), (self.user_client_1, 403), (self.admin_client_1, 200), (self.superuser_client, 200)],
|
||||
reverse('api:synclog-list'))
|
||||
self.batch_requests(
|
||||
[
|
||||
(self.anonymous_client, 403),
|
||||
(self.guest_client_1, 403),
|
||||
(self.user_client_1, 403),
|
||||
(self.admin_client_1, 200),
|
||||
(self.superuser_client, 200)
|
||||
],
|
||||
reverse('api:synclog-list')
|
||||
)
|
||||
|
||||
# verify log entry is returned
|
||||
r = self.admin_client_1.get(reverse('api:synclog-list'))
|
||||
@ -42,10 +50,21 @@ class TestApiSyncLog(TestViews):
|
||||
|
||||
def test_sync_log_update(self):
|
||||
# read only view
|
||||
r = self.admin_client_1.patch(reverse('api:synclog-detail', args={self.sync.id}), {'path': 'new'}, content_type='application/json')
|
||||
r = self.admin_client_1.patch(
|
||||
reverse(
|
||||
'api:synclog-detail',
|
||||
args={self.sync.id}
|
||||
),
|
||||
{'path': 'new'},
|
||||
content_type='application/json'
|
||||
)
|
||||
self.assertEqual(r.status_code, 405)
|
||||
|
||||
def test_sync_log_delete(self):
|
||||
# read only view
|
||||
r = self.admin_client_1.delete(reverse('api:synclog-detail', args={self.sync.id}))
|
||||
r = self.admin_client_1.delete(
|
||||
reverse(
|
||||
'api:synclog-detail',
|
||||
args={self.sync.id})
|
||||
)
|
||||
self.assertEqual(r.status_code, 405)
|
||||
|
@ -1,11 +1,9 @@
|
||||
import json
|
||||
|
||||
from django.contrib import auth
|
||||
from django.db.models import ProtectedError
|
||||
from django.urls import reverse
|
||||
|
||||
from cookbook.models import Storage, Sync
|
||||
from cookbook.tests.views.test_views import TestViews
|
||||
from django.contrib import auth
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
class TestApiSync(TestViews):
|
||||
@ -28,8 +26,16 @@ class TestApiSync(TestViews):
|
||||
|
||||
def test_sync_list(self):
|
||||
# verify view permissions are applied accordingly
|
||||
self.batch_requests([(self.anonymous_client, 403), (self.guest_client_1, 403), (self.user_client_1, 403), (self.admin_client_1, 200), (self.superuser_client, 200)],
|
||||
reverse('api:sync-list'))
|
||||
self.batch_requests(
|
||||
[
|
||||
(self.anonymous_client, 403),
|
||||
(self.guest_client_1, 403),
|
||||
(self.user_client_1, 403),
|
||||
(self.admin_client_1, 200),
|
||||
(self.superuser_client, 200)
|
||||
],
|
||||
reverse('api:sync-list')
|
||||
)
|
||||
|
||||
# verify sync is returned
|
||||
r = self.admin_client_1.get(reverse('api:sync-list'))
|
||||
@ -41,13 +47,22 @@ class TestApiSync(TestViews):
|
||||
|
||||
def test_sync_update(self):
|
||||
# can update sync as admin
|
||||
r = self.admin_client_1.patch(reverse('api:sync-detail', args={self.sync.id}), {'path': 'new'}, content_type='application/json')
|
||||
r = self.admin_client_1.patch(
|
||||
reverse(
|
||||
'api:sync-detail',
|
||||
args={self.sync.id}
|
||||
),
|
||||
{'path': 'new'},
|
||||
content_type='application/json'
|
||||
)
|
||||
response = json.loads(r.content)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(response['path'], 'new')
|
||||
|
||||
def test_sync_delete(self):
|
||||
# can delete sync as admin
|
||||
r = self.admin_client_1.delete(reverse('api:sync-detail', args={self.sync.id}))
|
||||
r = self.admin_client_1.delete(
|
||||
reverse('api:sync-detail', args={self.sync.id})
|
||||
)
|
||||
self.assertEqual(r.status_code, 204)
|
||||
self.assertEqual(Sync.objects.count(), 0)
|
||||
|
@ -1,9 +1,8 @@
|
||||
import json
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
from cookbook.models import Unit
|
||||
from cookbook.tests.views.test_views import TestViews
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
class TestApiUnit(TestViews):
|
||||
@ -19,8 +18,16 @@ class TestApiUnit(TestViews):
|
||||
|
||||
def test_keyword_list(self):
|
||||
# verify view permissions are applied accordingly
|
||||
self.batch_requests([(self.anonymous_client, 403), (self.guest_client_1, 403), (self.user_client_1, 200), (self.admin_client_1, 200), (self.superuser_client, 200)],
|
||||
reverse('api:unit-list'))
|
||||
self.batch_requests(
|
||||
[
|
||||
(self.anonymous_client, 403),
|
||||
(self.guest_client_1, 403),
|
||||
(self.user_client_1, 200),
|
||||
(self.admin_client_1, 200),
|
||||
(self.superuser_client, 200)
|
||||
],
|
||||
reverse('api:unit-list')
|
||||
)
|
||||
|
||||
# verify storage is returned
|
||||
r = self.user_client_1.get(reverse('api:unit-list'))
|
||||
@ -42,12 +49,21 @@ class TestApiUnit(TestViews):
|
||||
self.assertEqual(len(response), 1)
|
||||
|
||||
def test_keyword_update(self):
|
||||
r = self.user_client_1.patch(reverse('api:unit-detail', args={self.unit_1.id}), {'name': 'new'}, content_type='application/json')
|
||||
r = self.user_client_1.patch(
|
||||
reverse(
|
||||
'api:unit-detail',
|
||||
args={self.unit_1.id}
|
||||
),
|
||||
{'name': 'new'},
|
||||
content_type='application/json'
|
||||
)
|
||||
response = json.loads(r.content)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(response['name'], 'new')
|
||||
|
||||
def test_keyword_delete(self):
|
||||
r = self.user_client_1.delete(reverse('api:unit-detail', args={self.unit_1.id}))
|
||||
r = self.user_client_1.delete(
|
||||
reverse('api:unit-detail', args={self.unit_1.id})
|
||||
)
|
||||
self.assertEqual(r.status_code, 204)
|
||||
self.assertEqual(Unit.objects.count(), 1)
|
||||
|
@ -1,11 +1,7 @@
|
||||
import json
|
||||
|
||||
from cookbook.tests.views.test_views import TestViews
|
||||
from django.contrib import auth
|
||||
from django.urls import reverse
|
||||
|
||||
from cookbook.models import UserPreference
|
||||
from cookbook.tests.views.test_views import TestViews
|
||||
|
||||
|
||||
class TestApiUsername(TestViews):
|
||||
|
||||
@ -13,15 +9,33 @@ class TestApiUsername(TestViews):
|
||||
super(TestApiUsername, self).setUp()
|
||||
|
||||
def test_forbidden_methods(self):
|
||||
r = self.user_client_1.post(reverse('api:username-list'))
|
||||
r = self.user_client_1.post(
|
||||
reverse('api:username-list'))
|
||||
self.assertEqual(r.status_code, 405)
|
||||
|
||||
r = self.user_client_1.put(reverse('api:username-detail', args=[auth.get_user(self.user_client_1).pk]))
|
||||
r = self.user_client_1.put(
|
||||
reverse(
|
||||
'api:username-detail',
|
||||
args=[auth.get_user(self.user_client_1).pk])
|
||||
)
|
||||
self.assertEqual(r.status_code, 405)
|
||||
|
||||
r = self.user_client_1.delete(reverse('api:username-detail', args=[auth.get_user(self.user_client_1).pk]))
|
||||
r = self.user_client_1.delete(
|
||||
reverse(
|
||||
'api:username-detail',
|
||||
args=[auth.get_user(self.user_client_1).pk]
|
||||
)
|
||||
)
|
||||
self.assertEqual(r.status_code, 405)
|
||||
|
||||
def test_username_list(self):
|
||||
self.batch_requests([(self.anonymous_client, 403), (self.guest_client_1, 200), (self.user_client_1, 200), (self.admin_client_1, 200), (self.superuser_client, 200)],
|
||||
reverse('api:username-list'))
|
||||
self.batch_requests(
|
||||
[
|
||||
(self.anonymous_client, 403),
|
||||
(self.guest_client_1, 200),
|
||||
(self.user_client_1, 200),
|
||||
(self.admin_client_1, 200),
|
||||
(self.superuser_client, 200)
|
||||
],
|
||||
reverse('api:username-list')
|
||||
)
|
||||
|
@ -1,10 +1,9 @@
|
||||
import json
|
||||
|
||||
from django.contrib import auth
|
||||
from django.urls import reverse
|
||||
|
||||
from cookbook.models import UserPreference
|
||||
from cookbook.tests.views.test_views import TestViews
|
||||
from django.contrib import auth
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
class TestApiUserPreference(TestViews):
|
||||
@ -16,8 +15,13 @@ class TestApiUserPreference(TestViews):
|
||||
r = self.user_client_1.post(reverse('api:userpreference-list'))
|
||||
self.assertEqual(r.status_code, 201)
|
||||
response = json.loads(r.content)
|
||||
self.assertEqual(response['user'], auth.get_user(self.user_client_1).id)
|
||||
self.assertEqual(response['theme'], UserPreference._meta.get_field('theme').get_default())
|
||||
self.assertEqual(
|
||||
response['user'], auth.get_user(self.user_client_1).id
|
||||
)
|
||||
self.assertEqual(
|
||||
response['theme'],
|
||||
UserPreference._meta.get_field('theme').get_default()
|
||||
)
|
||||
|
||||
def test_preference_list(self):
|
||||
UserPreference.objects.create(user=auth.get_user(self.user_client_1))
|
||||
@ -28,7 +32,9 @@ class TestApiUserPreference(TestViews):
|
||||
self.assertEqual(r.status_code, 200)
|
||||
response = json.loads(r.content)
|
||||
self.assertEqual(len(response), 1)
|
||||
self.assertEqual(response[0]['user'], auth.get_user(self.user_client_1).id)
|
||||
self.assertEqual(
|
||||
response[0]['user'], auth.get_user(self.user_client_1).id
|
||||
)
|
||||
|
||||
# superusers can see all user prefs in list
|
||||
r = self.superuser_client.get(reverse('api:userpreference-list'))
|
||||
@ -40,47 +46,104 @@ class TestApiUserPreference(TestViews):
|
||||
UserPreference.objects.create(user=auth.get_user(self.user_client_1))
|
||||
UserPreference.objects.create(user=auth.get_user(self.guest_client_1))
|
||||
|
||||
self.batch_requests([(self.guest_client_1, 404), (self.user_client_1, 200), (self.user_client_2, 404), (self.anonymous_client, 403), (self.admin_client_1, 404), (self.superuser_client, 200)],
|
||||
reverse('api:userpreference-detail', args={auth.get_user(self.user_client_1).id}))
|
||||
self.batch_requests(
|
||||
[
|
||||
(self.guest_client_1, 404),
|
||||
(self.user_client_1, 200),
|
||||
(self.user_client_2, 404),
|
||||
(self.anonymous_client, 403),
|
||||
(self.admin_client_1, 404),
|
||||
(self.superuser_client, 200)
|
||||
],
|
||||
reverse(
|
||||
'api:userpreference-detail',
|
||||
args={auth.get_user(self.user_client_1).id}
|
||||
)
|
||||
)
|
||||
|
||||
def test_preference_update(self):
|
||||
UserPreference.objects.create(user=auth.get_user(self.user_client_1))
|
||||
UserPreference.objects.create(user=auth.get_user(self.guest_client_1))
|
||||
|
||||
# can update users preference
|
||||
r = self.user_client_1.put(reverse('api:userpreference-detail', args={auth.get_user(self.user_client_1).id}), {'theme': UserPreference.DARKLY}, content_type='application/json')
|
||||
r = self.user_client_1.put(
|
||||
reverse(
|
||||
'api:userpreference-detail',
|
||||
args={auth.get_user(self.user_client_1).id}
|
||||
),
|
||||
{'theme': UserPreference.DARKLY},
|
||||
content_type='application/json'
|
||||
)
|
||||
response = json.loads(r.content)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(response['theme'], UserPreference.DARKLY)
|
||||
|
||||
# cant set another users non existent pref
|
||||
r = self.user_client_1.put(reverse('api:userpreference-detail', args={auth.get_user(self.user_client_2).id}), {'theme': UserPreference.DARKLY}, content_type='application/json')
|
||||
r = self.user_client_1.put(
|
||||
reverse(
|
||||
'api:userpreference-detail',
|
||||
args={auth.get_user(self.user_client_2).id}
|
||||
),
|
||||
{'theme': UserPreference.DARKLY},
|
||||
content_type='application/json'
|
||||
)
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
# cant set another users existent pref
|
||||
r = self.user_client_2.put(reverse('api:userpreference-detail', args={auth.get_user(self.user_client_1).id}), {'theme': UserPreference.FLATLY}, content_type='application/json')
|
||||
r = self.user_client_2.put(
|
||||
reverse(
|
||||
'api:userpreference-detail',
|
||||
args={auth.get_user(self.user_client_1).id}
|
||||
),
|
||||
{'theme': UserPreference.FLATLY},
|
||||
content_type='application/json'
|
||||
)
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
# can set pref as superuser
|
||||
r = self.superuser_client.put(reverse('api:userpreference-detail', args={auth.get_user(self.user_client_1).id}), {'theme': UserPreference.FLATLY}, content_type='application/json')
|
||||
r = self.superuser_client.put(
|
||||
reverse(
|
||||
'api:userpreference-detail',
|
||||
args={auth.get_user(self.user_client_1).id}
|
||||
),
|
||||
{'theme': UserPreference.FLATLY},
|
||||
content_type='application/json'
|
||||
)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
def test_preference_delete(self):
|
||||
UserPreference.objects.create(user=auth.get_user(self.user_client_1))
|
||||
|
||||
# can delete own preference
|
||||
r = self.user_client_1.delete(reverse('api:userpreference-detail', args={auth.get_user(self.user_client_1).id}))
|
||||
r = self.user_client_1.delete(
|
||||
reverse(
|
||||
'api:userpreference-detail',
|
||||
args={auth.get_user(self.user_client_1).id}
|
||||
)
|
||||
)
|
||||
self.assertEqual(r.status_code, 204)
|
||||
self.assertEqual(UserPreference.objects.count(), 0)
|
||||
|
||||
UserPreference.objects.create(user=auth.get_user(self.user_client_1))
|
||||
UserPreference.objects.create(user=auth.get_user(self.user_client_1
|
||||
)
|
||||
)
|
||||
|
||||
# cant delete other preference
|
||||
r = self.user_client_2.delete(reverse('api:userpreference-detail', args={auth.get_user(self.user_client_1).id}))
|
||||
r = self.user_client_2.delete(
|
||||
reverse(
|
||||
'api:userpreference-detail',
|
||||
args={auth.get_user(self.user_client_1).id}
|
||||
)
|
||||
)
|
||||
self.assertEqual(r.status_code, 404)
|
||||
self.assertEqual(UserPreference.objects.count(), 1)
|
||||
|
||||
# superuser can delete everything
|
||||
r = self.superuser_client.delete(reverse('api:userpreference-detail', args={auth.get_user(self.user_client_1).id}))
|
||||
r = self.superuser_client.delete(
|
||||
reverse(
|
||||
'api:userpreference-detail',
|
||||
args={auth.get_user(self.user_client_1).id}
|
||||
)
|
||||
)
|
||||
self.assertEqual(r.status_code, 204)
|
||||
self.assertEqual(UserPreference.objects.count(), 0)
|
||||
|
@ -1,8 +1,7 @@
|
||||
from django.contrib import auth
|
||||
from django.urls import reverse
|
||||
|
||||
from cookbook.models import Comment, Recipe
|
||||
from cookbook.tests.views.test_views import TestViews
|
||||
from django.contrib import auth
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
class TestEditsComment(TestViews):
|
||||
@ -25,7 +24,17 @@ class TestEditsComment(TestViews):
|
||||
self.url = reverse('edit_comment', args=[self.comment.pk])
|
||||
|
||||
def test_new_comment(self):
|
||||
r = self.user_client_1.post(reverse('view_recipe', args=[self.recipe.pk]), {'comment-text': 'Test Comment Text', 'comment-recipe': self.recipe.pk})
|
||||
r = self.user_client_1.post(
|
||||
reverse(
|
||||
'view_recipe',
|
||||
args=[self.recipe.pk]
|
||||
),
|
||||
{
|
||||
'comment-text': 'Test Comment Text',
|
||||
'comment-recipe': self.recipe.pk
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
def test_edit_comment_permissions(self):
|
||||
|
@ -1,9 +1,8 @@
|
||||
from cookbook.models import Food, Recipe, Storage, Unit
|
||||
from cookbook.tests.views.test_views import TestViews
|
||||
from django.contrib import auth
|
||||
from django.urls import reverse
|
||||
|
||||
from cookbook.models import Recipe, Ingredient, Unit, Storage, Food
|
||||
from cookbook.tests.views.test_views import TestViews
|
||||
|
||||
|
||||
class TestEditsRecipe(TestViews):
|
||||
|
||||
@ -70,7 +69,17 @@ class TestEditsRecipe(TestViews):
|
||||
r = self.anonymous_client.get(url)
|
||||
self.assertEqual(r.status_code, 403)
|
||||
|
||||
r = self.user_client_1.put(url, {'name': 'Changed', 'working_time': 15, 'waiting_time': 15, 'keywords': [], 'steps': []}, content_type='application/json')
|
||||
r = self.user_client_1.put(
|
||||
url,
|
||||
{
|
||||
'name': 'Changed',
|
||||
'working_time': 15,
|
||||
'waiting_time': 15,
|
||||
'keywords': [],
|
||||
'steps': []
|
||||
},
|
||||
content_type='application/json'
|
||||
)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
recipe = Recipe.objects.get(pk=recipe.pk)
|
||||
@ -79,18 +88,39 @@ class TestEditsRecipe(TestViews):
|
||||
Food.objects.create(name='Egg')
|
||||
Unit.objects.create(name='g')
|
||||
|
||||
r = self.user_client_1.put(url, {'name': 'Changed', 'working_time': 15, 'waiting_time': 15, 'keywords': [],
|
||||
'steps': [{'ingredients': [
|
||||
{"food": {"name": "test food"}, "unit": {"name": "test unit"}, 'amount': 12, 'note': "test note"},
|
||||
{"food": {"name": "test food 2"}, "unit": {"name": "test unit 2"}, 'amount': 42, 'note': "test note 2"}
|
||||
]}]}, content_type='application/json')
|
||||
r = self.user_client_1.put(
|
||||
url,
|
||||
{
|
||||
'name': 'Changed',
|
||||
'working_time': 15,
|
||||
'waiting_time': 15,
|
||||
'keywords': [],
|
||||
'steps': [
|
||||
{
|
||||
'ingredients': [
|
||||
{
|
||||
'food': {'name': 'test food'},
|
||||
'unit': {'name': 'test unit'},
|
||||
'amount': 12, 'note': 'test note'
|
||||
},
|
||||
{
|
||||
'food': {'name': 'test food 2'},
|
||||
'unit': {'name': 'test unit 2'},
|
||||
'amount': 42, 'note': 'test note 2'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
content_type='application/json'
|
||||
)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(2, recipe.steps.first().ingredients.count())
|
||||
|
||||
with open('cookbook/tests/resources/image.jpg', 'rb') as file:
|
||||
with open('cookbook/tests/resources/image.jpg', 'rb') as file: # noqa: E501,F841
|
||||
pass # TODO new image tests
|
||||
|
||||
with open('cookbook/tests/resources/image.png', 'rb') as file:
|
||||
with open('cookbook/tests/resources/image.png', 'rb') as file: # noqa: E501,F841
|
||||
pass # TODO new image tests
|
||||
|
||||
def test_external_recipe_update(self):
|
||||
@ -117,7 +147,10 @@ class TestEditsRecipe(TestViews):
|
||||
r = self.anonymous_client.get(url)
|
||||
self.assertEqual(r.status_code, 302)
|
||||
|
||||
r = self.user_client_1.post(url, {'name': 'Test', 'working_time': 15, 'waiting_time': 15, })
|
||||
r = self.user_client_1.post(
|
||||
url,
|
||||
{'name': 'Test', 'working_time': 15, 'waiting_time': 15, }
|
||||
)
|
||||
recipe.refresh_from_db()
|
||||
self.assertEqual(recipe.working_time, 15)
|
||||
self.assertEqual(recipe.waiting_time, 15)
|
||||
|
@ -1,8 +1,7 @@
|
||||
from django.contrib import auth
|
||||
from django.urls import reverse
|
||||
|
||||
from cookbook.models import Storage
|
||||
from cookbook.tests.views.test_views import TestViews
|
||||
from django.contrib import auth
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
class TestEditsRecipe(TestViews):
|
||||
@ -21,13 +20,36 @@ class TestEditsRecipe(TestViews):
|
||||
self.url = reverse('edit_storage', args=[self.storage.pk])
|
||||
|
||||
def test_edit_storage(self):
|
||||
r = self.admin_client_1.post(self.url, {'name': 'NewStorage', 'password': '1234_pw', 'token': '1234_token', 'method': Storage.DROPBOX})
|
||||
r = self.admin_client_1.post(
|
||||
self.url,
|
||||
{
|
||||
'name': 'NewStorage',
|
||||
'password': '1234_pw',
|
||||
'token': '1234_token',
|
||||
'method': Storage.DROPBOX
|
||||
}
|
||||
)
|
||||
self.storage.refresh_from_db()
|
||||
self.assertEqual(self.storage.password, '1234_pw')
|
||||
self.assertEqual(self.storage.token, '1234_token')
|
||||
|
||||
r = self.admin_client_1.post(self.url, {'name': 'NewStorage', 'password': '1234_pw', 'token': '1234_token', 'method': 'not_a_valid_method'})
|
||||
self.assertFormError(r, 'form', 'method', ['Select a valid choice. not_a_valid_method is not one of the available choices.'])
|
||||
r = self.admin_client_1.post(
|
||||
self.url,
|
||||
{
|
||||
'name': 'NewStorage',
|
||||
'password': '1234_pw',
|
||||
'token': '1234_token',
|
||||
'method': 'not_a_valid_method'
|
||||
}
|
||||
)
|
||||
self.assertFormError(
|
||||
r,
|
||||
'form',
|
||||
'method',
|
||||
[
|
||||
'Select a valid choice. not_a_valid_method is not one of the available choices.' # noqa: E501
|
||||
]
|
||||
)
|
||||
|
||||
def test_edit_storage_permissions(self):
|
||||
r = self.anonymous_client.get(self.url)
|
||||
|
@ -7,6 +7,7 @@ from cookbook.tests.test_setup import TestBase
|
||||
|
||||
class TestEditsRecipe(TestBase):
|
||||
|
||||
# flake8: noqa
|
||||
def test_ld_json(self):
|
||||
test_list = [
|
||||
{'file': 'cookbook/tests/resources/websites/ld_json_1.html', 'result_length': 3218},
|
||||
@ -77,10 +78,10 @@ class TestEditsRecipe(TestBase):
|
||||
"3.5 l Wasser": (3.5, "l", "Wasser", ""),
|
||||
"400 g Karotte(n)": (400, "g", "Karotte(n)", "")
|
||||
}
|
||||
# for German you could say that if an ingredient does not have an amount and it starts with a lowercase letter, then that is a unit ("etwas", "evtl.")
|
||||
# does not apply to English tho
|
||||
# for German you could say that if an ingredient does not have
|
||||
# an amount # and it starts with a lowercase letter, then that
|
||||
# is a unit ("etwas", "evtl.") does not apply to English tho
|
||||
|
||||
errors = 0
|
||||
count = 0
|
||||
for key, val in expectations.items():
|
||||
count += 1
|
||||
|
@ -1,6 +1,6 @@
|
||||
from django.contrib import auth
|
||||
from django.contrib.auth.models import User, Group
|
||||
from django.test import TestCase, Client
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.test import Client, TestCase
|
||||
|
||||
|
||||
class TestBase(TestCase):
|
||||
@ -38,8 +38,14 @@ class TestBase(TestCase):
|
||||
user.is_superuser = True
|
||||
user.save()
|
||||
|
||||
def batch_requests(self, clients, url, method='get', payload={}, content_type=''):
|
||||
def batch_requests(
|
||||
self, clients, url, method='get', payload={}, content_type=''
|
||||
):
|
||||
for c in clients:
|
||||
if method == 'get':
|
||||
r = c[0].get(url)
|
||||
self.assertEqual(r.status_code, c[1], msg=f'GET request failed for user {auth.get_user(c[0])} when testing url {url}')
|
||||
self.assertEqual(
|
||||
r.status_code,
|
||||
c[1],
|
||||
msg=f'GET request failed for user {auth.get_user(c[0])} when testing url {url}' # noqa: E501
|
||||
)
|
||||
|
@ -1,8 +1,7 @@
|
||||
from django.contrib import auth
|
||||
from django.urls import reverse
|
||||
|
||||
from cookbook.models import Recipe
|
||||
from cookbook.tests.views.test_views import TestViews
|
||||
from django.contrib import auth
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
class TestViewsApi(TestViews):
|
||||
|
@ -1,6 +1,5 @@
|
||||
from django.urls import reverse
|
||||
|
||||
from cookbook.tests.views.test_views import TestViews
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
class TestViewsGeneral(TestViews):
|
||||
@ -19,11 +18,29 @@ class TestViewsGeneral(TestViews):
|
||||
|
||||
def test_books(self):
|
||||
url = reverse('view_books')
|
||||
self.batch_requests([(self.anonymous_client, 302), (self.guest_client_1, 302), (self.user_client_1, 200), (self.admin_client_1, 200), (self.superuser_client, 200)], url)
|
||||
self.batch_requests(
|
||||
[
|
||||
(self.anonymous_client, 302),
|
||||
(self.guest_client_1, 302),
|
||||
(self.user_client_1, 200),
|
||||
(self.admin_client_1, 200),
|
||||
(self.superuser_client, 200)
|
||||
],
|
||||
url
|
||||
)
|
||||
|
||||
def test_plan(self):
|
||||
url = reverse('view_plan')
|
||||
self.batch_requests([(self.anonymous_client, 302), (self.guest_client_1, 302), (self.user_client_1, 200), (self.admin_client_1, 200), (self.superuser_client, 200)], url)
|
||||
self.batch_requests(
|
||||
[
|
||||
(self.anonymous_client, 302),
|
||||
(self.guest_client_1, 302),
|
||||
(self.user_client_1, 200),
|
||||
(self.admin_client_1, 200),
|
||||
(self.superuser_client, 200)
|
||||
],
|
||||
url
|
||||
)
|
||||
|
||||
def test_plan_entry(self):
|
||||
# TODO add appropriate test
|
||||
@ -31,28 +48,91 @@ class TestViewsGeneral(TestViews):
|
||||
|
||||
def test_shopping(self):
|
||||
url = reverse('view_shopping')
|
||||
self.batch_requests([(self.anonymous_client, 302), (self.guest_client_1, 302), (self.user_client_1, 200), (self.admin_client_1, 200), (self.superuser_client, 200)], url)
|
||||
self.batch_requests(
|
||||
[
|
||||
(self.anonymous_client, 302),
|
||||
(self.guest_client_1, 302),
|
||||
(self.user_client_1, 200),
|
||||
(self.admin_client_1, 200),
|
||||
(self.superuser_client, 200)
|
||||
],
|
||||
url
|
||||
)
|
||||
|
||||
def test_settings(self):
|
||||
url = reverse('view_settings')
|
||||
self.batch_requests([(self.anonymous_client, 302), (self.guest_client_1, 200), (self.user_client_1, 200), (self.admin_client_1, 200), (self.superuser_client, 200)], url)
|
||||
self.batch_requests(
|
||||
[
|
||||
(self.anonymous_client, 302),
|
||||
(self.guest_client_1, 200),
|
||||
(self.user_client_1, 200),
|
||||
(self.admin_client_1, 200),
|
||||
(self.superuser_client, 200)
|
||||
],
|
||||
url
|
||||
)
|
||||
|
||||
def test_history(self):
|
||||
url = reverse('view_history')
|
||||
self.batch_requests([(self.anonymous_client, 302), (self.guest_client_1, 200), (self.user_client_1, 200), (self.admin_client_1, 200), (self.superuser_client, 200)], url)
|
||||
self.batch_requests(
|
||||
[
|
||||
(self.anonymous_client, 302),
|
||||
(self.guest_client_1, 200),
|
||||
(self.user_client_1, 200),
|
||||
(self.admin_client_1, 200),
|
||||
(self.superuser_client, 200)
|
||||
],
|
||||
url
|
||||
)
|
||||
|
||||
def test_system(self):
|
||||
url = reverse('view_system')
|
||||
self.batch_requests([(self.anonymous_client, 302), (self.guest_client_1, 302), (self.user_client_1, 302), (self.admin_client_1, 200), (self.superuser_client, 200)], url)
|
||||
self.batch_requests(
|
||||
[
|
||||
(self.anonymous_client, 302),
|
||||
(self.guest_client_1, 302),
|
||||
(self.user_client_1, 302),
|
||||
(self.admin_client_1, 200),
|
||||
(self.superuser_client, 200)
|
||||
],
|
||||
url
|
||||
)
|
||||
|
||||
def test_setup(self):
|
||||
url = reverse('view_setup')
|
||||
self.batch_requests([(self.anonymous_client, 302), (self.guest_client_1, 302), (self.user_client_1, 302), (self.admin_client_1, 302), (self.superuser_client, 302)], url)
|
||||
self.batch_requests(
|
||||
[
|
||||
(self.anonymous_client, 302),
|
||||
(self.guest_client_1, 302),
|
||||
(self.user_client_1, 302),
|
||||
(self.admin_client_1, 302),
|
||||
(self.superuser_client, 302)
|
||||
],
|
||||
url
|
||||
)
|
||||
|
||||
def test_markdown_info(self):
|
||||
url = reverse('docs_markdown')
|
||||
self.batch_requests([(self.anonymous_client, 200), (self.guest_client_1, 200), (self.user_client_1, 200), (self.admin_client_1, 200), (self.superuser_client, 200)], url)
|
||||
self.batch_requests(
|
||||
[
|
||||
(self.anonymous_client, 200),
|
||||
(self.guest_client_1, 200),
|
||||
(self.user_client_1, 200),
|
||||
(self.admin_client_1, 200),
|
||||
(self.superuser_client, 200)
|
||||
],
|
||||
url
|
||||
)
|
||||
|
||||
def test_api_info(self):
|
||||
url = reverse('docs_api')
|
||||
self.batch_requests([(self.anonymous_client, 302), (self.guest_client_1, 200), (self.user_client_1, 200), (self.admin_client_1, 200), (self.superuser_client, 200)], url)
|
||||
self.batch_requests(
|
||||
[
|
||||
(self.anonymous_client, 302),
|
||||
(self.guest_client_1, 200),
|
||||
(self.user_client_1, 200),
|
||||
(self.admin_client_1, 200),
|
||||
(self.superuser_client, 200)
|
||||
],
|
||||
url
|
||||
)
|
||||
|
@ -1,11 +1,10 @@
|
||||
import uuid
|
||||
|
||||
from django.contrib import auth
|
||||
from django.urls import reverse
|
||||
|
||||
from cookbook.helper.permission_helper import share_link_valid
|
||||
from cookbook.models import Recipe, ShareLink
|
||||
from cookbook.tests.views.test_views import TestViews
|
||||
from django.contrib import auth
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
class TestViewsGeneral(TestViews):
|
||||
@ -31,14 +30,23 @@ class TestViewsGeneral(TestViews):
|
||||
self.assertIsNotNone(share)
|
||||
self.assertTrue(share_link_valid(internal_recipe, share.uuid))
|
||||
|
||||
url = reverse('view_recipe', kwargs={'pk': internal_recipe.pk, 'share': share.uuid})
|
||||
url = reverse(
|
||||
'view_recipe',
|
||||
kwargs={'pk': internal_recipe.pk, 'share': share.uuid}
|
||||
)
|
||||
r = self.anonymous_client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
url = reverse('view_recipe', kwargs={'pk': (internal_recipe.pk + 1), 'share': share.uuid})
|
||||
url = reverse(
|
||||
'view_recipe',
|
||||
kwargs={'pk': (internal_recipe.pk + 1), 'share': share.uuid}
|
||||
)
|
||||
r = self.anonymous_client.get(url)
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
url = reverse('view_recipe', kwargs={'pk': internal_recipe.pk, 'share': uuid.uuid4()})
|
||||
url = reverse(
|
||||
'view_recipe',
|
||||
kwargs={'pk': internal_recipe.pk, 'share': uuid.uuid4()}
|
||||
)
|
||||
r = self.anonymous_client.get(url)
|
||||
self.assertEqual(r.status_code, 302)
|
||||
|
132
cookbook/urls.py
132
cookbook/urls.py
@ -1,13 +1,18 @@
|
||||
from pydoc import locate
|
||||
|
||||
from django.urls import path, include
|
||||
from django.urls import include, path
|
||||
from django.views.generic import TemplateView
|
||||
from recipes.version import VERSION_NUMBER
|
||||
from rest_framework import routers
|
||||
from rest_framework.schemas import get_schema_view
|
||||
|
||||
from .views import *
|
||||
from cookbook.views import api, import_export
|
||||
from cookbook.helper import dal
|
||||
|
||||
from .models import (Comment, Food, InviteLink, Keyword, MealPlan, Recipe,
|
||||
RecipeBook, RecipeBookEntry, RecipeImport, ShoppingList,
|
||||
Storage, Sync, SyncLog, get_model_name)
|
||||
from .views import api, data, delete, edit, import_export, lists, new, views
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
router.register(r'user-name', api.UserNameViewSet, basename='username')
|
||||
router.register(r'user-preference', api.UserPreferenceViewSet)
|
||||
@ -41,43 +46,100 @@ urlpatterns = [
|
||||
path('shopping/<int:pk>', views.shopping_list, name='view_shopping'),
|
||||
path('settings/', views.user_settings, name='view_settings'),
|
||||
path('history/', views.history, name='view_history'),
|
||||
path('offline/', views.offline, name='view_offline'),
|
||||
path(
|
||||
'service-worker.js', (
|
||||
TemplateView.as_view(
|
||||
template_name="service-worker.js",
|
||||
content_type='application/javascript',
|
||||
)
|
||||
),
|
||||
name='service_worker'
|
||||
),
|
||||
path('test/', views.test, name='view_test'),
|
||||
|
||||
path('import/', import_export.import_recipe, name='view_import'),
|
||||
path('export/', import_export.export_recipe, name='view_export'),
|
||||
|
||||
path('view/recipe/<int:pk>', views.recipe_view, name='view_recipe'),
|
||||
path('view/recipe/<int:pk>/<slug:share>', views.recipe_view, name='view_recipe'),
|
||||
path(
|
||||
'view/recipe/<int:pk>/<slug:share>',
|
||||
views.recipe_view,
|
||||
name='view_recipe'
|
||||
),
|
||||
|
||||
path('new/recipe-import/<int:import_id>/', new.create_new_external_recipe, name='new_recipe_import'),
|
||||
path(
|
||||
'new/recipe-import/<int:import_id>/',
|
||||
new.create_new_external_recipe,
|
||||
name='new_recipe_import'
|
||||
),
|
||||
path('new/share-link/<int:pk>/', new.share_link, name='new_share_link'),
|
||||
|
||||
path('edit/recipe/<int:pk>/', edit.switch_recipe, name='edit_recipe'),
|
||||
path('edit/recipe/internal/<int:pk>/', edit.internal_recipe_update, name='edit_internal_recipe'), # for internal use only
|
||||
path('edit/recipe/external/<int:pk>/', edit.ExternalRecipeUpdate.as_view(), name='edit_external_recipe'), # for internal use only
|
||||
path('edit/recipe/convert/<int:pk>/', edit.convert_recipe, name='edit_convert_recipe'), # for internal use only
|
||||
|
||||
# for internal use only
|
||||
path(
|
||||
'edit/recipe/internal/<int:pk>/',
|
||||
edit.internal_recipe_update,
|
||||
name='edit_internal_recipe'
|
||||
),
|
||||
path(
|
||||
'edit/recipe/external/<int:pk>/',
|
||||
edit.ExternalRecipeUpdate.as_view(),
|
||||
name='edit_external_recipe'
|
||||
),
|
||||
path(
|
||||
'edit/recipe/convert/<int:pk>/',
|
||||
edit.convert_recipe,
|
||||
name='edit_convert_recipe'
|
||||
),
|
||||
|
||||
path('edit/storage/<int:pk>/', edit.edit_storage, name='edit_storage'),
|
||||
path('edit/ingredient/', edit.edit_ingredients, name='edit_food'),
|
||||
|
||||
path('delete/recipe-source/<int:pk>/', delete.delete_recipe_source, name='delete_recipe_source'),
|
||||
path(
|
||||
'delete/recipe-source/<int:pk>/',
|
||||
delete.delete_recipe_source,
|
||||
name='delete_recipe_source'
|
||||
),
|
||||
|
||||
path('data/sync', data.sync, name='data_sync'), # TODO move to generic "new" view
|
||||
# TODO move to generic "new" view
|
||||
path('data/sync', data.sync, name='data_sync'),
|
||||
path('data/batch/edit', data.batch_edit, name='data_batch_edit'),
|
||||
path('data/batch/import', data.batch_import, name='data_batch_import'),
|
||||
path('data/sync/wait', data.sync_wait, name='data_sync_wait'),
|
||||
path('data/statistics', data.statistics, name='data_stats'),
|
||||
path('data/import/url', data.import_url, name='data_import_url'),
|
||||
|
||||
path('api/get_external_file_link/<int:recipe_id>/', api.get_external_file_link, name='api_get_external_file_link'),
|
||||
path('api/get_recipe_file/<int:recipe_id>/', api.get_recipe_file, name='api_get_recipe_file'),
|
||||
path(
|
||||
'api/get_external_file_link/<int:recipe_id>/',
|
||||
api.get_external_file_link,
|
||||
name='api_get_external_file_link'
|
||||
),
|
||||
path(
|
||||
'api/get_recipe_file/<int:recipe_id>/',
|
||||
api.get_recipe_file,
|
||||
name='api_get_recipe_file'
|
||||
),
|
||||
path('api/sync_all/', api.sync_all, name='api_sync'),
|
||||
path('api/log_cooking/<int:recipe_id>/', api.log_cooking, name='api_log_cooking'),
|
||||
path('api/plan-ical/<slug:from_date>/<slug:to_date>/', api.get_plan_ical, name='api_get_plan_ical'),
|
||||
path('api/recipe-from-url/', api.recipe_from_url, name='api_recipe_from_url'),
|
||||
path(
|
||||
'api/log_cooking/<int:recipe_id>/',
|
||||
api.log_cooking,
|
||||
name='api_log_cooking'
|
||||
),
|
||||
path(
|
||||
'api/plan-ical/<slug:from_date>/<slug:to_date>/',
|
||||
api.get_plan_ical,
|
||||
name='api_get_plan_ical'
|
||||
),
|
||||
path(
|
||||
'api/recipe-from-url/', api.recipe_from_url, name='api_recipe_from_url'
|
||||
),
|
||||
path('api/backup/', api.get_backup, name='api_backup'),
|
||||
|
||||
path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'),
|
||||
path(
|
||||
'dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'
|
||||
),
|
||||
path('dal/food/', dal.IngredientsAutocomplete.as_view(), name='dal_food'),
|
||||
path('dal/unit/', dal.UnitAutocomplete.as_view(), name='dal_unit'),
|
||||
|
||||
@ -90,24 +152,50 @@ urlpatterns = [
|
||||
), name='openapi-schema'),
|
||||
|
||||
path('api/', include((router.urls, 'api'))),
|
||||
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
||||
path(
|
||||
'api-auth/',
|
||||
include('rest_framework.urls', namespace='rest_framework')
|
||||
),
|
||||
|
||||
]
|
||||
|
||||
generic_models = (Recipe, RecipeImport, Storage, RecipeBook, MealPlan, SyncLog, Sync, Comment, RecipeBookEntry, Keyword, Food, ShoppingList, InviteLink)
|
||||
generic_models = (
|
||||
Recipe, RecipeImport, Storage, RecipeBook, MealPlan, SyncLog, Sync,
|
||||
Comment, RecipeBookEntry, Keyword, Food, ShoppingList, InviteLink
|
||||
)
|
||||
|
||||
for m in generic_models:
|
||||
py_name = get_model_name(m)
|
||||
url_name = py_name.replace('_', '-')
|
||||
|
||||
if c := locate(f'cookbook.views.new.{m.__name__}Create'):
|
||||
urlpatterns.append(path(f'new/{url_name}/', c.as_view(), name=f'new_{py_name}'))
|
||||
urlpatterns.append(
|
||||
path(
|
||||
f'new/{url_name}/', c.as_view(), name=f'new_{py_name}'
|
||||
)
|
||||
)
|
||||
|
||||
if c := locate(f'cookbook.views.edit.{m.__name__}Update'):
|
||||
urlpatterns.append(path(f'edit/{url_name}/<int:pk>/', c.as_view(), name=f'edit_{py_name}'))
|
||||
urlpatterns.append(
|
||||
path(
|
||||
f'edit/{url_name}/<int:pk>/',
|
||||
c.as_view(),
|
||||
name=f'edit_{py_name}'
|
||||
)
|
||||
)
|
||||
|
||||
if c := getattr(lists, py_name, None):
|
||||
urlpatterns.append(path(f'list/{url_name}/', c, name=f'list_{py_name}'))
|
||||
urlpatterns.append(
|
||||
path(
|
||||
f'list/{url_name}/', c, name=f'list_{py_name}'
|
||||
)
|
||||
)
|
||||
|
||||
if c := locate(f'cookbook.views.delete.{m.__name__}Delete'):
|
||||
urlpatterns.append(path(f'delete/{url_name}/<int:pk>/', c.as_view(), name=f'delete_{py_name}'))
|
||||
urlpatterns.append(
|
||||
path(
|
||||
f'delete/{url_name}/<int:pk>/',
|
||||
c.as_view(),
|
||||
name=f'delete_{py_name}'
|
||||
)
|
||||
)
|
||||
|
@ -1,7 +1,19 @@
|
||||
from cookbook.views.views import *
|
||||
from cookbook.views.api import *
|
||||
from cookbook.views.data import *
|
||||
from cookbook.views.edit import *
|
||||
from cookbook.views.new import *
|
||||
from cookbook.views.lists import *
|
||||
from cookbook.views.delete import *
|
||||
import cookbook.views.api
|
||||
import cookbook.views.data
|
||||
import cookbook.views.delete
|
||||
import cookbook.views.edit
|
||||
import cookbook.views.import_export
|
||||
import cookbook.views.lists
|
||||
import cookbook.views.new
|
||||
import cookbook.views.views
|
||||
|
||||
__all__ = [
|
||||
'api',
|
||||
'data',
|
||||
'delete',
|
||||
'edit',
|
||||
'import_export',
|
||||
'lists',
|
||||
'new',
|
||||
'views',
|
||||
]
|
||||
|
@ -4,7 +4,6 @@ import re
|
||||
import uuid
|
||||
|
||||
import requests
|
||||
from PIL import Image
|
||||
from annoying.decorators import ajax_request
|
||||
from annoying.functions import get_object_or_None
|
||||
from django.contrib import messages
|
||||
@ -12,28 +11,45 @@ from django.contrib.auth.models import User
|
||||
from django.core import management
|
||||
from django.core.files import File
|
||||
from django.db.models import Q
|
||||
from django.http import HttpResponse, FileResponse, JsonResponse
|
||||
from django.http import FileResponse, HttpResponse, JsonResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.utils import timezone, dateformat
|
||||
from django.utils import timezone
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic.base import View
|
||||
from icalendar import Calendar, Event
|
||||
from rest_framework import viewsets, permissions, decorators
|
||||
from PIL import Image
|
||||
from rest_framework import decorators, permissions, viewsets
|
||||
from rest_framework.exceptions import APIException
|
||||
from rest_framework.mixins import RetrieveModelMixin, UpdateModelMixin, ListModelMixin
|
||||
from rest_framework.parsers import JSONParser, FileUploadParser, MultiPartParser
|
||||
from rest_framework.mixins import (ListModelMixin, RetrieveModelMixin,
|
||||
UpdateModelMixin)
|
||||
from rest_framework.parsers import MultiPartParser
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import ViewSetMixin
|
||||
|
||||
from cookbook.helper.permission_helper import group_required, CustomIsOwner, CustomIsAdmin, CustomIsUser, CustomIsGuest, CustomIsShare, CustomIsShared
|
||||
from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest,
|
||||
CustomIsOwner, CustomIsShare,
|
||||
CustomIsShared, CustomIsUser,
|
||||
group_required)
|
||||
from cookbook.helper.recipe_url_import import get_from_html
|
||||
from cookbook.models import Recipe, Sync, Storage, CookLog, MealPlan, MealType, ViewLog, UserPreference, RecipeBook, Ingredient, Food, Step, Keyword, Unit, SyncLog, ShoppingListRecipe, ShoppingList, ShoppingListEntry
|
||||
from cookbook.models import (CookLog, Food, Ingredient, Keyword, MealPlan,
|
||||
MealType, Recipe, RecipeBook, ShoppingList,
|
||||
ShoppingListEntry, ShoppingListRecipe, Step,
|
||||
Storage, Sync, SyncLog, Unit, UserPreference,
|
||||
ViewLog)
|
||||
from cookbook.provider.dropbox import Dropbox
|
||||
from cookbook.provider.nextcloud import Nextcloud
|
||||
from cookbook.serializer import MealPlanSerializer, MealTypeSerializer, RecipeSerializer, ViewLogSerializer, UserNameSerializer, UserPreferenceSerializer, RecipeBookSerializer, IngredientSerializer, FoodSerializer, StepSerializer, \
|
||||
KeywordSerializer, RecipeImageSerializer, StorageSerializer, SyncSerializer, SyncLogSerializer, UnitSerializer, ShoppingListSerializer, ShoppingListRecipeSerializer, ShoppingListEntrySerializer, ShoppingListEntryCheckedSerializer, \
|
||||
ShoppingListAutoSyncSerializer
|
||||
from cookbook.serializer import (FoodSerializer, IngredientSerializer,
|
||||
KeywordSerializer, MealPlanSerializer,
|
||||
MealTypeSerializer, RecipeBookSerializer,
|
||||
RecipeImageSerializer, RecipeSerializer,
|
||||
ShoppingListAutoSyncSerializer,
|
||||
ShoppingListEntrySerializer,
|
||||
ShoppingListRecipeSerializer,
|
||||
ShoppingListSerializer, StepSerializer,
|
||||
StorageSerializer, SyncLogSerializer,
|
||||
SyncSerializer, UnitSerializer,
|
||||
UserNameSerializer, UserPreferenceSerializer,
|
||||
ViewLogSerializer)
|
||||
|
||||
|
||||
class UserNameViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
@ -54,8 +70,10 @@ class UserNameViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
filter_list = self.request.query_params.get('filter_list', None)
|
||||
if filter_list is not None:
|
||||
queryset = queryset.filter(pk__in=json.loads(filter_list))
|
||||
except ValueError as e:
|
||||
raise APIException(_('Parameter filter_list incorrectly formatted'))
|
||||
except ValueError:
|
||||
raise APIException(
|
||||
_('Parameter filter_list incorrectly formatted')
|
||||
)
|
||||
|
||||
return queryset
|
||||
|
||||
@ -118,7 +136,8 @@ class KeywordViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
||||
list:
|
||||
optional parameters
|
||||
|
||||
- **query**: search keywords for a string contained in the keyword name (case in-sensitive)
|
||||
- **query**: search keywords for a string contained
|
||||
in the keyword name (case in-sensitive)
|
||||
- **limit**: limits the amount of returned results
|
||||
"""
|
||||
queryset = Keyword.objects.all()
|
||||
@ -138,7 +157,12 @@ class FoodViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
||||
permission_classes = [CustomIsUser]
|
||||
|
||||
|
||||
class RecipeBookViewSet(RetrieveModelMixin, UpdateModelMixin, ListModelMixin, viewsets.GenericViewSet):
|
||||
class RecipeBookViewSet(
|
||||
RetrieveModelMixin,
|
||||
UpdateModelMixin,
|
||||
ListModelMixin,
|
||||
viewsets.GenericViewSet
|
||||
):
|
||||
queryset = RecipeBook.objects.all()
|
||||
serializer_class = RecipeBookSerializer
|
||||
permission_classes = [CustomIsOwner, CustomIsAdmin]
|
||||
@ -163,7 +187,10 @@ class MealPlanViewSet(viewsets.ModelViewSet):
|
||||
permission_classes = [permissions.IsAuthenticated] # TODO fix permissions
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = MealPlan.objects.filter(Q(created_by=self.request.user) | Q(shared=self.request.user)).distinct().all()
|
||||
queryset = MealPlan.objects.filter(
|
||||
Q(created_by=self.request.user) |
|
||||
Q(shared=self.request.user)
|
||||
).distinct().all()
|
||||
|
||||
from_date = self.request.query_params.get('from_date', None)
|
||||
if from_date is not None:
|
||||
@ -177,15 +204,16 @@ class MealPlanViewSet(viewsets.ModelViewSet):
|
||||
|
||||
class MealTypeViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
list:
|
||||
returns list of meal types created by the requesting user ordered by the order field
|
||||
returns list of meal types created by the
|
||||
requesting user ordered by the order field.
|
||||
"""
|
||||
queryset = MealType.objects.order_by('order').all()
|
||||
serializer_class = MealTypeSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = MealType.objects.order_by('order', 'id').filter(created_by=self.request.user).all()
|
||||
queryset = MealType.objects.order_by('order', 'id') \
|
||||
.filter(created_by=self.request.user).all()
|
||||
return queryset
|
||||
|
||||
|
||||
@ -206,12 +234,14 @@ class RecipeViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
||||
list:
|
||||
optional parameters
|
||||
|
||||
- **query**: search recipes for a string contained in the recipe name (case in-sensitive)
|
||||
- **query**: search recipes for a string contained
|
||||
in the recipe name (case in-sensitive)
|
||||
- **limit**: limits the amount of returned results
|
||||
"""
|
||||
queryset = Recipe.objects.all()
|
||||
serializer_class = RecipeSerializer
|
||||
permission_classes = [CustomIsShare | CustomIsGuest] # TODO split read and write permission for meal plan guest
|
||||
# TODO split read and write permission for meal plan guest
|
||||
permission_classes = [CustomIsShare | CustomIsGuest]
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
@ -231,7 +261,9 @@ class RecipeViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
||||
)
|
||||
def image(self, request, pk):
|
||||
obj = self.get_object()
|
||||
serializer = self.serializer_class(obj, data=request.data, partial=True)
|
||||
serializer = self.serializer_class(
|
||||
obj, data=request.data, partial=True
|
||||
)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
@ -275,7 +307,9 @@ class ShoppingListViewSet(viewsets.ModelViewSet):
|
||||
def get_queryset(self):
|
||||
if self.request.user.is_superuser:
|
||||
return self.queryset
|
||||
return self.queryset.filter(Q(created_by=self.request.user) | Q(shared=self.request.user)).all()
|
||||
return self.queryset.filter(
|
||||
Q(created_by=self.request.user) | Q(shared=self.request.user)
|
||||
).all()
|
||||
|
||||
def get_serializer_class(self):
|
||||
autosync = self.request.query_params.get('autosync', None)
|
||||
@ -290,7 +324,8 @@ class ViewLogViewSet(viewsets.ModelViewSet):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = ViewLog.objects.filter(created_by=self.request.user).all()[:5]
|
||||
queryset = ViewLog.objects \
|
||||
.filter(created_by=self.request.user).all()[:5]
|
||||
return queryset
|
||||
|
||||
|
||||
@ -307,7 +342,8 @@ def get_recipe_provider(recipe):
|
||||
|
||||
def update_recipe_links(recipe):
|
||||
if not recipe.link:
|
||||
recipe.link = get_recipe_provider(recipe).get_share_link(recipe) # TODO response validation in apis
|
||||
# TODO response validation in apis
|
||||
recipe.link = get_recipe_provider(recipe).get_share_link(recipe)
|
||||
|
||||
recipe.save()
|
||||
|
||||
@ -346,10 +382,14 @@ def sync_all(request):
|
||||
error = True
|
||||
|
||||
if not error:
|
||||
messages.add_message(request, messages.SUCCESS, _('Sync successful!'))
|
||||
messages.add_message(
|
||||
request, messages.SUCCESS, _('Sync successful!')
|
||||
)
|
||||
return redirect('list_recipe_import')
|
||||
else:
|
||||
messages.add_message(request, messages.ERROR, _('Error synchronizing with Storage'))
|
||||
messages.add_message(
|
||||
request, messages.ERROR, _('Error synchronizing with Storage')
|
||||
)
|
||||
return redirect('list_recipe_import')
|
||||
|
||||
|
||||
@ -374,7 +414,9 @@ def log_cooking(request, recipe_id):
|
||||
|
||||
@group_required('user')
|
||||
def get_plan_ical(request, from_date, to_date):
|
||||
queryset = MealPlan.objects.filter(Q(created_by=request.user) | Q(shared=request.user)).distinct().all()
|
||||
queryset = MealPlan.objects.filter(
|
||||
Q(created_by=request.user) | Q(shared=request.user)
|
||||
).distinct().all()
|
||||
|
||||
if from_date is not None:
|
||||
queryset = queryset.filter(date__gte=from_date)
|
||||
@ -394,7 +436,7 @@ def get_plan_ical(request, from_date, to_date):
|
||||
cal.add_component(event)
|
||||
|
||||
response = FileResponse(io.BytesIO(cal.to_ical()))
|
||||
response["Content-Disposition"] = f'attachment; filename=meal_plan_{from_date}-{to_date}.ics'
|
||||
response["Content-Disposition"] = f'attachment; filename=meal_plan_{from_date}-{to_date}.ics' # noqa: E501
|
||||
|
||||
return response
|
||||
|
||||
@ -403,14 +445,28 @@ def get_plan_ical(request, from_date, to_date):
|
||||
def recipe_from_url(request):
|
||||
url = request.POST['url']
|
||||
|
||||
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36'}
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36' # noqa: E501
|
||||
}
|
||||
try:
|
||||
response = requests.get(url, headers=headers)
|
||||
except requests.exceptions.ConnectionError:
|
||||
return JsonResponse({'error': True, 'msg': _('The requested page could not be found.')}, status=400)
|
||||
return JsonResponse(
|
||||
{
|
||||
'error': True,
|
||||
'msg': _('The requested page could not be found.')
|
||||
},
|
||||
status=400
|
||||
)
|
||||
|
||||
if response.status_code == 403:
|
||||
return JsonResponse({'error': True, 'msg': _('The requested page refused to provide any information (Status Code 403).')}, status=400)
|
||||
return JsonResponse(
|
||||
{
|
||||
'error': True,
|
||||
'msg': _('The requested page refused to provide any information (Status Code 403).') # noqa: E501
|
||||
},
|
||||
status=400
|
||||
)
|
||||
return get_from_html(response.text, url)
|
||||
|
||||
|
||||
@ -419,9 +475,11 @@ def get_backup(request):
|
||||
return HttpResponse('', status=403)
|
||||
|
||||
buf = io.StringIO()
|
||||
management.call_command('dumpdata', exclude=['contenttypes', 'auth'], stdout=buf)
|
||||
management.call_command(
|
||||
'dumpdata', exclude=['contenttypes', 'auth'], stdout=buf
|
||||
)
|
||||
|
||||
response = FileResponse(buf.getvalue())
|
||||
response["Content-Disposition"] = f'attachment; filename=backup{date_format(timezone.now(), format="SHORT_DATETIME_FORMAT", use_l10n=True)}.json'
|
||||
response["Content-Disposition"] = f'attachment; filename=backup{date_format(timezone.now(), format="SHORT_DATETIME_FORMAT", use_l10n=True)}.json' # noqa: E501
|
||||
|
||||
return response
|
||||
|
@ -1,22 +1,25 @@
|
||||
import json
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from io import BytesIO
|
||||
|
||||
import requests
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
from django.contrib import messages
|
||||
from django.core.files import File
|
||||
from django.db.transaction import atomic
|
||||
from django.utils.translation import gettext as _
|
||||
from django.http import HttpResponseRedirect, HttpResponse
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import ngettext
|
||||
from django_tables2 import RequestConfig
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
|
||||
from cookbook.forms import SyncForm, BatchEditForm
|
||||
from cookbook.helper.permission_helper import group_required, has_group_permission
|
||||
from cookbook.models import *
|
||||
from cookbook.forms import BatchEditForm, SyncForm
|
||||
from cookbook.helper.permission_helper import (group_required,
|
||||
has_group_permission)
|
||||
from cookbook.models import (Comment, Food, Ingredient, Keyword, Recipe,
|
||||
RecipeImport, Step, Sync, Unit)
|
||||
from cookbook.tables import SyncTable
|
||||
|
||||
|
||||
@ -24,7 +27,10 @@ from cookbook.tables import SyncTable
|
||||
def sync(request):
|
||||
if request.method == "POST":
|
||||
if not has_group_permission(request.user, ['admin']):
|
||||
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
|
||||
messages.add_message(
|
||||
request, messages.ERROR,
|
||||
_('You do not have the required permissions to view this page!') # noqa: E501
|
||||
)
|
||||
return HttpResponseRedirect(reverse('data_sync'))
|
||||
form = SyncForm(request.POST)
|
||||
if form.is_valid():
|
||||
@ -38,9 +44,15 @@ def sync(request):
|
||||
form = SyncForm()
|
||||
|
||||
monitored_paths = SyncTable(Sync.objects.all())
|
||||
RequestConfig(request, paginate={'per_page': 25}).configure(monitored_paths)
|
||||
RequestConfig(
|
||||
request, paginate={'per_page': 25}
|
||||
).configure(monitored_paths)
|
||||
|
||||
return render(request, 'batch/monitor.html', {'form': form, 'monitored_paths': monitored_paths})
|
||||
return render(
|
||||
request,
|
||||
'batch/monitor.html',
|
||||
{'form': form, 'monitored_paths': monitored_paths}
|
||||
)
|
||||
|
||||
|
||||
@group_required('user')
|
||||
@ -52,7 +64,13 @@ def sync_wait(request):
|
||||
def batch_import(request):
|
||||
imports = RecipeImport.objects.all()
|
||||
for new_recipe in imports:
|
||||
recipe = Recipe(name=new_recipe.name, file_path=new_recipe.file_path, storage=new_recipe.storage, file_uid=new_recipe.file_uid, created_by=request.user)
|
||||
recipe = Recipe(
|
||||
name=new_recipe.name,
|
||||
file_path=new_recipe.file_path,
|
||||
storage=new_recipe.storage,
|
||||
file_uid=new_recipe.file_uid,
|
||||
created_by=request.user
|
||||
)
|
||||
recipe.save()
|
||||
new_recipe.delete()
|
||||
|
||||
@ -115,7 +133,8 @@ def import_url(request):
|
||||
recipe.steps.add(step)
|
||||
|
||||
for kw in data['keywords']:
|
||||
if kw['id'] != "null" and (k := Keyword.objects.filter(id=kw['id']).first()):
|
||||
if kw['id'] != "null" \
|
||||
and (k := Keyword.objects.filter(id=kw['id']).first()):
|
||||
recipe.keywords.add(k)
|
||||
elif data['all_keywords']:
|
||||
k = Keyword.objects.create(name=kw['text'])
|
||||
@ -125,10 +144,14 @@ def import_url(request):
|
||||
ingredient = Ingredient()
|
||||
|
||||
if ing['ingredient']['text'] != '':
|
||||
ingredient.food, f_created = Food.objects.get_or_create(name=ing['ingredient']['text'])
|
||||
ingredient.food, f_created = Food.objects.get_or_create(
|
||||
name=ing['ingredient']['text']
|
||||
)
|
||||
|
||||
if ing['unit'] and ing['unit']['text'] != '':
|
||||
ingredient.unit, u_created = Unit.objects.get_or_create(name=ing['unit']['text'])
|
||||
ingredient.unit, u_created = Unit.objects.get_or_create(
|
||||
name=ing['unit']['text']
|
||||
)
|
||||
|
||||
# TODO properly handle no_amount recipes
|
||||
if isinstance(ing['amount'], str):
|
||||
@ -137,7 +160,8 @@ def import_url(request):
|
||||
except ValueError:
|
||||
ingredient.no_amount = True
|
||||
pass
|
||||
elif isinstance(ing['amount'], float) or isinstance(ing['amount'], int):
|
||||
elif isinstance(ing['amount'], float) \
|
||||
or isinstance(ing['amount'], int):
|
||||
ingredient.amount = ing['amount']
|
||||
ingredient.note = ing['note'] if 'note' in ing else ''
|
||||
|
||||
@ -158,7 +182,9 @@ def import_url(request):
|
||||
|
||||
im_io = BytesIO()
|
||||
img.save(im_io, 'PNG', quality=70)
|
||||
recipe.image = File(im_io, name=f'{uuid.uuid4()}_{recipe.pk}.png')
|
||||
recipe.image = File(
|
||||
im_io, name=f'{uuid.uuid4()}_{recipe.pk}.png'
|
||||
)
|
||||
recipe.save()
|
||||
except UnidentifiedImageError:
|
||||
pass
|
||||
|
@ -1,15 +1,17 @@
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.db.models import ProtectedError
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse_lazy, reverse
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import DeleteView
|
||||
|
||||
from cookbook.helper.permission_helper import group_required, GroupRequiredMixin, OwnerRequiredMixin
|
||||
from cookbook.models import Recipe, Sync, Keyword, RecipeImport, Storage, Comment, RecipeBook, \
|
||||
RecipeBookEntry, MealPlan, Food, InviteLink
|
||||
from cookbook.helper.permission_helper import (GroupRequiredMixin,
|
||||
OwnerRequiredMixin,
|
||||
group_required)
|
||||
from cookbook.models import (Comment, InviteLink, Keyword, MealPlan, Recipe,
|
||||
RecipeBook, RecipeBookEntry, RecipeImport,
|
||||
Storage, Sync)
|
||||
from cookbook.provider.dropbox import Dropbox
|
||||
from cookbook.provider.nextcloud import Nextcloud
|
||||
|
||||
@ -31,7 +33,8 @@ def delete_recipe_source(request, pk):
|
||||
recipe = get_object_or_404(Recipe, pk=pk)
|
||||
|
||||
if recipe.storage.method == Storage.DROPBOX:
|
||||
Dropbox.delete_file(recipe) # TODO central location to handle storage type switches
|
||||
# TODO central location to handle storage type switches
|
||||
Dropbox.delete_file(recipe)
|
||||
if recipe.storage.method == Storage.NEXTCLOUD:
|
||||
Nextcloud.delete_file(recipe)
|
||||
|
||||
@ -94,7 +97,11 @@ class StorageDelete(GroupRequiredMixin, DeleteView):
|
||||
try:
|
||||
return self.delete(request, *args, **kwargs)
|
||||
except ProtectedError:
|
||||
messages.add_message(request, messages.WARNING, _('Could not delete this storage backend as it is used in at least one monitor.'))
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.WARNING,
|
||||
_('Could not delete this storage backend as it is used in at least one monitor.') # noqa: E501
|
||||
)
|
||||
return HttpResponseRedirect(reverse('list_storage'))
|
||||
|
||||
|
||||
@ -128,10 +135,16 @@ class RecipeBookEntryDelete(GroupRequiredMixin, DeleteView):
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
obj = self.get_object()
|
||||
if not (obj.book.created_by == request.user or request.user.is_superuser):
|
||||
messages.add_message(request, messages.ERROR, _('You cannot interact with this object as it is not owned by you!'))
|
||||
if not (obj.book.created_by == request.user
|
||||
or request.user.is_superuser):
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.ERROR,
|
||||
_('You cannot interact with this object as it is not owned by you!') # noqa: E501
|
||||
)
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
return super(RecipeBookEntryDelete, self).dispatch(request, *args, **kwargs)
|
||||
return super(RecipeBookEntryDelete, self) \
|
||||
.dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(RecipeBookEntryDelete, self).get_context_data(**kwargs)
|
||||
|
@ -7,12 +7,16 @@ from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import UpdateView
|
||||
|
||||
from cookbook.forms import ExternalRecipeForm, KeywordForm, StorageForm, SyncForm, CommentForm, \
|
||||
MealPlanForm, UnitMergeForm, RecipeBookForm, FoodForm, FoodMergeForm
|
||||
from cookbook.helper.permission_helper import OwnerRequiredMixin
|
||||
from cookbook.helper.permission_helper import group_required, GroupRequiredMixin
|
||||
from cookbook.models import Recipe, Sync, Keyword, RecipeImport, Storage, Comment, Ingredient, RecipeBook, \
|
||||
MealPlan, Food, MealType
|
||||
from cookbook.forms import (CommentForm, ExternalRecipeForm, FoodForm,
|
||||
FoodMergeForm, KeywordForm, MealPlanForm,
|
||||
RecipeBookForm, StorageForm, SyncForm,
|
||||
UnitMergeForm)
|
||||
from cookbook.helper.permission_helper import (GroupRequiredMixin,
|
||||
OwnerRequiredMixin,
|
||||
group_required)
|
||||
from cookbook.models import (Comment, Food, Ingredient, Keyword, MealPlan,
|
||||
MealType, Recipe, RecipeBook, RecipeImport,
|
||||
Storage, Sync)
|
||||
from cookbook.provider.dropbox import Dropbox
|
||||
from cookbook.provider.nextcloud import Nextcloud
|
||||
|
||||
@ -40,7 +44,9 @@ def convert_recipe(request, pk):
|
||||
def internal_recipe_update(request, pk):
|
||||
recipe_instance = get_object_or_404(Recipe, pk=pk)
|
||||
|
||||
return render(request, 'forms/edit_internal_recipe.html', {'recipe': recipe_instance})
|
||||
return render(
|
||||
request, 'forms/edit_internal_recipe.html', {'recipe': recipe_instance}
|
||||
)
|
||||
|
||||
|
||||
class SyncUpdate(GroupRequiredMixin, UpdateView):
|
||||
@ -99,7 +105,9 @@ def edit_storage(request, pk):
|
||||
instance = get_object_or_404(Storage, pk=pk)
|
||||
|
||||
if not (instance.created_by == request.user or request.user.is_superuser):
|
||||
messages.add_message(request, messages.ERROR, _('You cannot edit this storage!'))
|
||||
messages.add_message(
|
||||
request, messages.ERROR, _('You cannot edit this storage!')
|
||||
)
|
||||
return HttpResponseRedirect(reverse('list_storage'))
|
||||
|
||||
if request.method == "POST":
|
||||
@ -118,16 +126,26 @@ def edit_storage(request, pk):
|
||||
|
||||
instance.save()
|
||||
|
||||
messages.add_message(request, messages.SUCCESS, _('Storage saved!'))
|
||||
messages.add_message(
|
||||
request, messages.SUCCESS, _('Storage saved!')
|
||||
)
|
||||
else:
|
||||
messages.add_message(request, messages.ERROR, _('There was an error updating this storage backend!'))
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.ERROR,
|
||||
_('There was an error updating this storage backend!')
|
||||
)
|
||||
else:
|
||||
pseudo_instance = instance
|
||||
pseudo_instance.password = '__NO__CHANGE__'
|
||||
pseudo_instance.token = '__NO__CHANGE__'
|
||||
form = StorageForm(instance=pseudo_instance)
|
||||
|
||||
return render(request, 'generic/edit_template.html', {'form': form, 'title': _('Storage')})
|
||||
return render(
|
||||
request,
|
||||
'generic/edit_template.html',
|
||||
{'form': form, 'title': _('Storage')}
|
||||
)
|
||||
|
||||
|
||||
class CommentUpdate(OwnerRequiredMixin, UpdateView):
|
||||
@ -141,7 +159,9 @@ class CommentUpdate(OwnerRequiredMixin, UpdateView):
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(CommentUpdate, self).get_context_data(**kwargs)
|
||||
context['title'] = _("Comment")
|
||||
context['view_url'] = reverse('view_recipe', args=[self.object.recipe.pk])
|
||||
context['view_url'] = reverse(
|
||||
'view_recipe', args=[self.object.recipe.pk]
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
@ -186,7 +206,8 @@ class MealPlanUpdate(OwnerRequiredMixin, UpdateView):
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = self.form_class(**self.get_form_kwargs())
|
||||
form.fields['meal_type'].queryset = MealType.objects.filter(created_by=self.request.user).all()
|
||||
form.fields['meal_type'].queryset = MealType.objects \
|
||||
.filter(created_by=self.request.user).all()
|
||||
return form
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@ -206,17 +227,28 @@ class ExternalRecipeUpdate(GroupRequiredMixin, UpdateView):
|
||||
old_recipe = Recipe.objects.get(pk=self.object.pk)
|
||||
if not old_recipe.name == self.object.name:
|
||||
if self.object.storage.method == Storage.DROPBOX:
|
||||
Dropbox.rename_file(old_recipe, self.object.name) # TODO central location to handle storage type switches
|
||||
# TODO central location to handle storage type switches
|
||||
Dropbox.rename_file(old_recipe, self.object.name)
|
||||
if self.object.storage.method == Storage.NEXTCLOUD:
|
||||
Nextcloud.rename_file(old_recipe, self.object.name)
|
||||
|
||||
self.object.file_path = os.path.dirname(self.object.file_path) + '/' + self.object.name + os.path.splitext(self.object.file_path)[1]
|
||||
self.object.file_path = "%s/%s%s" % (
|
||||
os.path.dirname(self.object.file_path),
|
||||
self.object.name,
|
||||
os.path.splitext(self.object.file_path)[1]
|
||||
)
|
||||
|
||||
messages.add_message(self.request, messages.SUCCESS, _('Changes saved!'))
|
||||
messages.add_message(
|
||||
self.request, messages.SUCCESS, _('Changes saved!')
|
||||
)
|
||||
return super(ExternalRecipeUpdate, self).form_valid(form)
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.add_message(self.request, messages.ERROR, _('Error saving changes!'))
|
||||
messages.add_message(
|
||||
self.request,
|
||||
messages.ERROR,
|
||||
_('Error saving changes!')
|
||||
)
|
||||
return super(ExternalRecipeUpdate, self).form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
@ -227,7 +259,9 @@ class ExternalRecipeUpdate(GroupRequiredMixin, UpdateView):
|
||||
context['title'] = _("Recipe")
|
||||
context['view_url'] = reverse('view_recipe', args=[self.object.pk])
|
||||
if self.object.storage:
|
||||
context['delete_external_url'] = reverse('delete_recipe_source', args=[self.object.pk])
|
||||
context['delete_external_url'] = reverse(
|
||||
'delete_recipe_source', args=[self.object.pk]
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
@ -240,16 +274,23 @@ def edit_ingredients(request):
|
||||
new_unit = units_form.cleaned_data['new_unit']
|
||||
old_unit = units_form.cleaned_data['old_unit']
|
||||
if new_unit != old_unit:
|
||||
recipe_ingredients = Ingredient.objects.filter(unit=old_unit).all()
|
||||
recipe_ingredients = Ingredient.objects \
|
||||
.filter(unit=old_unit).all()
|
||||
for i in recipe_ingredients:
|
||||
i.unit = new_unit
|
||||
i.save()
|
||||
|
||||
old_unit.delete()
|
||||
success = True
|
||||
messages.add_message(request, messages.SUCCESS, _('Units merged!'))
|
||||
messages.add_message(
|
||||
request, messages.SUCCESS, _('Units merged!')
|
||||
)
|
||||
else:
|
||||
messages.add_message(request, messages.ERROR, _('Cannot merge with the same object!'))
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.ERROR,
|
||||
_('Cannot merge with the same object!')
|
||||
)
|
||||
|
||||
food_form = FoodMergeForm(request.POST, prefix=FoodMergeForm.prefix)
|
||||
if food_form.is_valid():
|
||||
@ -263,9 +304,15 @@ def edit_ingredients(request):
|
||||
|
||||
old_food.delete()
|
||||
success = True
|
||||
messages.add_message(request, messages.SUCCESS, _('Foods merged!'))
|
||||
messages.add_message(
|
||||
request, messages.SUCCESS, _('Foods merged!')
|
||||
)
|
||||
else:
|
||||
messages.add_message(request, messages.ERROR, _('Cannot merge with the same object!'))
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.ERROR,
|
||||
_('Cannot merge with the same object!')
|
||||
)
|
||||
|
||||
if success:
|
||||
units_form = UnitMergeForm()
|
||||
@ -274,4 +321,8 @@ def edit_ingredients(request):
|
||||
units_form = UnitMergeForm()
|
||||
food_form = FoodMergeForm()
|
||||
|
||||
return render(request, 'forms/ingredients.html', {'units_form': units_form, 'food_form': food_form})
|
||||
return render(
|
||||
request,
|
||||
'forms/ingredients.html',
|
||||
{'units_form': units_form, 'food_form': food_form}
|
||||
)
|
||||
|
@ -5,7 +5,7 @@ from json import JSONDecodeError
|
||||
|
||||
from django.contrib import messages
|
||||
from django.core.files.base import ContentFile
|
||||
from django.http import HttpResponseRedirect, HttpResponse
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext as _
|
||||
@ -23,7 +23,9 @@ def import_recipe(request):
|
||||
form = ImportForm(request.POST)
|
||||
if form.is_valid():
|
||||
try:
|
||||
data = json.loads(re.sub(r'"id":([0-9])+,', '', form.cleaned_data['recipe']))
|
||||
data = json.loads(
|
||||
re.sub(r'"id":([0-9])+,', '', form.cleaned_data['recipe'])
|
||||
)
|
||||
|
||||
sr = RecipeSerializer(data=data)
|
||||
if sr.is_valid():
|
||||
@ -34,18 +36,39 @@ def import_recipe(request):
|
||||
try:
|
||||
fmt, img = data['image'].split(';base64,')
|
||||
ext = fmt.split('/')[-1]
|
||||
recipe.image = ContentFile(base64.b64decode(img), name=f'{recipe.pk}.{ext}') # TODO possible security risk, maybe some checks needed
|
||||
# TODO possible security risk,
|
||||
# maybe some checks needed
|
||||
recipe.image = (ContentFile(
|
||||
base64.b64decode(img),
|
||||
name=f'{recipe.pk}.{ext}')
|
||||
)
|
||||
recipe.save()
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
messages.add_message(request, messages.SUCCESS, _('Recipe imported successfully!'))
|
||||
return HttpResponseRedirect(reverse_lazy('view_recipe', args=[recipe.pk]))
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.SUCCESS,
|
||||
_('Recipe imported successfully!')
|
||||
)
|
||||
return HttpResponseRedirect(
|
||||
reverse_lazy('view_recipe', args=[recipe.pk])
|
||||
)
|
||||
else:
|
||||
messages.add_message(request, messages.ERROR, _('Something went wrong during the import!'))
|
||||
messages.add_message(request, messages.WARNING, sr.errors)
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.ERROR,
|
||||
_('Something went wrong during the import!')
|
||||
)
|
||||
messages.add_message(
|
||||
request, messages.WARNING, sr.errors
|
||||
)
|
||||
except JSONDecodeError:
|
||||
messages.add_message(request, messages.ERROR, _('Could not parse the supplied JSON!'))
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.ERROR,
|
||||
_('Could not parse the supplied JSON!')
|
||||
)
|
||||
|
||||
else:
|
||||
form = ImportForm()
|
||||
@ -65,18 +88,23 @@ def export_recipe(request):
|
||||
|
||||
if recipe.image and form.cleaned_data['image']:
|
||||
with open(recipe.image.path, 'rb') as img_f:
|
||||
export['image'] = f'data:image/png;base64,{base64.b64encode(img_f.read()).decode("utf-8")}'
|
||||
export['image'] = f'data:image/png;base64,{base64.b64encode(img_f.read()).decode("utf-8")}' # noqa: E501
|
||||
|
||||
json_string = JSONRenderer().render(export).decode("utf-8")
|
||||
|
||||
if form.cleaned_data['download']:
|
||||
response = HttpResponse(json_string, content_type='text/plain')
|
||||
response['Content-Disposition'] = f'attachment; filename={recipe.name}.json'
|
||||
response = HttpResponse(
|
||||
json_string, content_type='text/plain'
|
||||
)
|
||||
response['Content-Disposition'] = f'attachment; filename={recipe.name}.json' # noqa: E501
|
||||
return response
|
||||
|
||||
context['export'] = re.sub(r'"id":([0-9])+,', '', json_string)
|
||||
else:
|
||||
form.add_error('recipe', _('External recipes cannot be exported, please share the file directly or select an internal recipe.'))
|
||||
form.add_error(
|
||||
'recipe',
|
||||
_('External recipes cannot be exported, please share the file directly or select an internal recipe.') # noqa: E501
|
||||
)
|
||||
else:
|
||||
form = ExportForm()
|
||||
recipe = request.GET.get('r')
|
||||
|
@ -1,6 +1,5 @@
|
||||
from datetime import datetime
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.db.models import Q
|
||||
from django.db.models.functions import Lower
|
||||
from django.shortcuts import render
|
||||
@ -9,8 +8,11 @@ from django_tables2 import RequestConfig
|
||||
|
||||
from cookbook.filters import IngredientFilter, ShoppingListFilter
|
||||
from cookbook.helper.permission_helper import group_required
|
||||
from cookbook.models import Keyword, SyncLog, RecipeImport, Storage, Food, ShoppingList, InviteLink
|
||||
from cookbook.tables import KeywordTable, ImportLogTable, RecipeImportTable, StorageTable, IngredientTable, ShoppingListTable, InviteLinkTable
|
||||
from cookbook.models import (Food, InviteLink, Keyword, RecipeImport,
|
||||
ShoppingList, Storage, SyncLog)
|
||||
from cookbook.tables import (ImportLogTable, IngredientTable, InviteLinkTable,
|
||||
KeywordTable, RecipeImportTable,
|
||||
ShoppingListTable, StorageTable)
|
||||
|
||||
|
||||
@group_required('user')
|
||||
@ -18,15 +20,27 @@ def keyword(request):
|
||||
table = KeywordTable(Keyword.objects.all())
|
||||
RequestConfig(request, paginate={'per_page': 25}).configure(table)
|
||||
|
||||
return render(request, 'generic/list_template.html', {'title': _("Keyword"), 'table': table, 'create_url': 'new_keyword'})
|
||||
return render(
|
||||
request,
|
||||
'generic/list_template.html',
|
||||
{'title': _("Keyword"), 'table': table, 'create_url': 'new_keyword'}
|
||||
)
|
||||
|
||||
|
||||
@group_required('admin')
|
||||
def sync_log(request):
|
||||
table = ImportLogTable(SyncLog.objects.all().order_by(Lower('created_at').desc()))
|
||||
table = ImportLogTable(
|
||||
SyncLog.objects.all().order_by(
|
||||
Lower('created_at').desc()
|
||||
)
|
||||
)
|
||||
RequestConfig(request, paginate={'per_page': 25}).configure(table)
|
||||
|
||||
return render(request, 'generic/list_template.html', {'title': _("Import Log"), 'table': table})
|
||||
return render(
|
||||
request,
|
||||
'generic/list_template.html',
|
||||
{'title': _("Import Log"), 'table': table}
|
||||
)
|
||||
|
||||
|
||||
@group_required('user')
|
||||
@ -35,27 +49,52 @@ def recipe_import(request):
|
||||
|
||||
RequestConfig(request, paginate={'per_page': 25}).configure(table)
|
||||
|
||||
return render(request, 'generic/list_template.html', {'title': _("Discovery"), 'table': table, 'import_btn': True})
|
||||
return render(
|
||||
request,
|
||||
'generic/list_template.html',
|
||||
{'title': _("Discovery"), 'table': table, 'import_btn': True}
|
||||
)
|
||||
|
||||
|
||||
@group_required('user')
|
||||
def food(request):
|
||||
f = IngredientFilter(request.GET, queryset=Food.objects.all().order_by('pk'))
|
||||
f = IngredientFilter(
|
||||
request.GET,
|
||||
queryset=Food.objects.all().order_by('pk')
|
||||
)
|
||||
|
||||
table = IngredientTable(f.qs)
|
||||
RequestConfig(request, paginate={'per_page': 25}).configure(table)
|
||||
|
||||
return render(request, 'generic/list_template.html', {'title': _("Ingredients"), 'table': table, 'filter': f})
|
||||
return render(
|
||||
request,
|
||||
'generic/list_template.html',
|
||||
{'title': _("Ingredients"), 'table': table, 'filter': f}
|
||||
)
|
||||
|
||||
|
||||
@group_required('user')
|
||||
def shopping_list(request):
|
||||
f = ShoppingListFilter(request.GET, queryset=ShoppingList.objects.filter(Q(created_by=request.user) | Q(shared=request.user)).all().order_by('finished', 'created_at'))
|
||||
f = ShoppingListFilter(
|
||||
request.GET,
|
||||
queryset=ShoppingList.objects.filter(
|
||||
Q(created_by=request.user) |
|
||||
Q(shared=request.user)
|
||||
).all().order_by('finished', 'created_at'))
|
||||
|
||||
table = ShoppingListTable(f.qs)
|
||||
RequestConfig(request, paginate={'per_page': 25}).configure(table)
|
||||
|
||||
return render(request, 'generic/list_template.html', {'title': _("Shopping Lists"), 'table': table, 'filter': f, 'create_url': 'view_shopping'})
|
||||
return render(
|
||||
request,
|
||||
'generic/list_template.html',
|
||||
{
|
||||
'title': _("Shopping Lists"),
|
||||
'table': table,
|
||||
'filter': f,
|
||||
'create_url': 'view_shopping'
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@group_required('admin')
|
||||
@ -63,12 +102,31 @@ def storage(request):
|
||||
table = StorageTable(Storage.objects.all())
|
||||
RequestConfig(request, paginate={'per_page': 25}).configure(table)
|
||||
|
||||
return render(request, 'generic/list_template.html', {'title': _("Storage Backend"), 'table': table, 'create_url': 'new_storage'})
|
||||
return render(
|
||||
request,
|
||||
'generic/list_template.html',
|
||||
{
|
||||
'title': _("Storage Backend"),
|
||||
'table': table,
|
||||
'create_url': 'new_storage'
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@group_required('admin')
|
||||
def invite_link(request):
|
||||
table = InviteLinkTable(InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None).all())
|
||||
table = InviteLinkTable(
|
||||
InviteLink.objects.filter(
|
||||
valid_until__gte=datetime.today(), used_by=None
|
||||
).all())
|
||||
RequestConfig(request, paginate={'per_page': 25}).configure(table)
|
||||
|
||||
return render(request, 'generic/list_template.html', {'title': _("Invite Links"), 'table': table, 'create_url': 'new_invite_link'})
|
||||
return render(
|
||||
request,
|
||||
'generic/list_template.html',
|
||||
{
|
||||
'title': _("Invite Links"),
|
||||
'table': table,
|
||||
'create_url': 'new_invite_link'
|
||||
}
|
||||
)
|
||||
|
@ -3,15 +3,17 @@ from datetime import datetime
|
||||
|
||||
from django.contrib import messages
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.urls import reverse_lazy, reverse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import CreateView
|
||||
|
||||
from cookbook.forms import ImportRecipeForm, RecipeImport, KeywordForm, Storage, StorageForm, InternalRecipeForm, \
|
||||
RecipeBookForm, MealPlanForm, InviteLinkForm
|
||||
from cookbook.helper.permission_helper import GroupRequiredMixin, group_required
|
||||
from cookbook.models import Keyword, Recipe, RecipeBook, MealPlan, ShareLink, MealType, Step, InviteLink
|
||||
from cookbook.forms import (ImportRecipeForm, InviteLinkForm, KeywordForm,
|
||||
MealPlanForm, RecipeBookForm, Storage, StorageForm)
|
||||
from cookbook.helper.permission_helper import (GroupRequiredMixin,
|
||||
group_required)
|
||||
from cookbook.models import (InviteLink, Keyword, MealPlan, MealType, Recipe,
|
||||
RecipeBook, RecipeImport, ShareLink, Step)
|
||||
|
||||
|
||||
class RecipeCreate(GroupRequiredMixin, CreateView):
|
||||
@ -26,7 +28,9 @@ class RecipeCreate(GroupRequiredMixin, CreateView):
|
||||
obj.internal = True
|
||||
obj.save()
|
||||
obj.steps.add(Step.objects.create())
|
||||
return HttpResponseRedirect(reverse('edit_recipe', kwargs={'pk': obj.pk}))
|
||||
return HttpResponseRedirect(
|
||||
reverse('edit_recipe', kwargs={'pk': obj.pk})
|
||||
)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('edit_recipe', kwargs={'pk': self.object.pk})
|
||||
@ -41,7 +45,9 @@ class RecipeCreate(GroupRequiredMixin, CreateView):
|
||||
def share_link(request, pk):
|
||||
recipe = get_object_or_404(Recipe, pk=pk)
|
||||
link = ShareLink.objects.create(recipe=recipe, created_by=request.user)
|
||||
return HttpResponseRedirect(reverse('view_recipe', kwargs={'pk': pk, 'share': link.uuid}))
|
||||
return HttpResponseRedirect(
|
||||
reverse('view_recipe', kwargs={'pk': pk, 'share': link.uuid})
|
||||
)
|
||||
|
||||
|
||||
class KeywordCreate(GroupRequiredMixin, CreateView):
|
||||
@ -68,7 +74,9 @@ class StorageCreate(GroupRequiredMixin, CreateView):
|
||||
obj = form.save(commit=False)
|
||||
obj.created_by = self.request.user
|
||||
obj.save()
|
||||
return HttpResponseRedirect(reverse('edit_storage', kwargs={'pk': obj.pk}))
|
||||
return HttpResponseRedirect(
|
||||
reverse('edit_storage', kwargs={'pk': obj.pk})
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(StorageCreate, self).get_context_data(**kwargs)
|
||||
@ -95,14 +103,25 @@ def create_new_external_recipe(request, import_id):
|
||||
|
||||
RecipeImport.objects.get(id=import_id).delete()
|
||||
|
||||
messages.add_message(request, messages.SUCCESS, _('Imported new recipe!'))
|
||||
messages.add_message(
|
||||
request, messages.SUCCESS, _('Imported new recipe!')
|
||||
)
|
||||
return redirect('list_recipe_import')
|
||||
else:
|
||||
messages.add_message(request, messages.ERROR, _('There was an error importing this recipe!'))
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.ERROR,
|
||||
_('There was an error importing this recipe!')
|
||||
)
|
||||
else:
|
||||
new_recipe = RecipeImport.objects.get(id=import_id)
|
||||
form = ImportRecipeForm(
|
||||
initial={'file_path': new_recipe.file_path, 'name': new_recipe.name, 'file_uid': new_recipe.file_uid})
|
||||
initial={
|
||||
'file_path': new_recipe.file_path,
|
||||
'name': new_recipe.name,
|
||||
'file_uid': new_recipe.file_uid
|
||||
}
|
||||
)
|
||||
|
||||
return render(request, 'forms/edit_import_recipe.html', {'form': form})
|
||||
|
||||
@ -135,14 +154,28 @@ class MealPlanCreate(GroupRequiredMixin, CreateView):
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = self.form_class(**self.get_form_kwargs())
|
||||
form.fields['meal_type'].queryset = MealType.objects.filter(created_by=self.request.user).all()
|
||||
form.fields['meal_type'].queryset = MealType.objects.filter(
|
||||
created_by=self.request.user
|
||||
).all()
|
||||
return form
|
||||
|
||||
def get_initial(self):
|
||||
return dict(
|
||||
meal_type=self.request.GET['meal'] if 'meal' in self.request.GET else None,
|
||||
date=datetime.strptime(self.request.GET['date'], '%Y-%m-%d') if 'date' in self.request.GET else None,
|
||||
shared=self.request.user.userpreference.plan_share.all() if self.request.user.userpreference.plan_share else None
|
||||
meal_type=(
|
||||
self.request.GET['meal']
|
||||
if 'meal' in self.request.GET
|
||||
else None
|
||||
),
|
||||
date=(
|
||||
datetime.strptime(self.request.GET['date'], '%Y-%m-%d')
|
||||
if 'date' in self.request.GET
|
||||
else None
|
||||
),
|
||||
shared=(
|
||||
self.request.user.userpreference.plan_share.all()
|
||||
if self.request.user.userpreference.plan_share
|
||||
else None
|
||||
)
|
||||
)
|
||||
|
||||
def form_valid(self, form):
|
||||
@ -159,7 +192,7 @@ class MealPlanCreate(GroupRequiredMixin, CreateView):
|
||||
if recipe:
|
||||
if re.match(r'^([0-9])+$', recipe):
|
||||
if Recipe.objects.filter(pk=int(recipe)).exists():
|
||||
context['default_recipe'] = Recipe.objects.get(pk=int(recipe))
|
||||
context['default_recipe'] = Recipe.objects.get(pk=int(recipe)) # noqa: E501
|
||||
|
||||
return context
|
||||
|
||||
|
@ -1,35 +1,40 @@
|
||||
import copy
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from typing import re
|
||||
from uuid import UUID
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import update_session_auth_hash, authenticate
|
||||
from django.contrib.auth import update_session_auth_hash
|
||||
from django.contrib.auth.forms import PasswordChangeForm
|
||||
from django.contrib.auth.password_validation import validate_password
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Q, Avg
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import Avg, Q
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils import timezone
|
||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||
from django_tables2 import RequestConfig
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from django.conf import settings
|
||||
from django_tables2 import RequestConfig
|
||||
from rest_framework.authtoken.models import Token
|
||||
|
||||
from cookbook.filters import RecipeFilter
|
||||
from cookbook.forms import *
|
||||
from cookbook.forms import (CommentForm, Recipe, RecipeBookEntryForm, User,
|
||||
UserCreateForm, UserNameForm, UserPreference,
|
||||
UserPreferenceForm)
|
||||
from cookbook.helper.permission_helper import group_required, share_link_valid
|
||||
from cookbook.tables import RecipeTable, RecipeTableSmall, CookLogTable, ViewLogTable
|
||||
|
||||
from recipes.version import *
|
||||
from cookbook.models import (Comment, CookLog, InviteLink, MealPlan,
|
||||
RecipeBook, RecipeBookEntry, ViewLog)
|
||||
from cookbook.tables import (CookLogTable, RecipeTable, RecipeTableSmall,
|
||||
ViewLogTable)
|
||||
from recipes.version import BUILD_REF, VERSION_NUMBER
|
||||
|
||||
|
||||
def index(request):
|
||||
if not request.user.is_authenticated:
|
||||
if User.objects.count() < 1 and 'django.contrib.auth.backends.RemoteUserBackend' not in settings.AUTHENTICATION_BACKENDS:
|
||||
if (User.objects.count() < 1
|
||||
and 'django.contrib.auth.backends.RemoteUserBackend' not in settings.AUTHENTICATION_BACKENDS): # noqa: E501
|
||||
return HttpResponseRedirect(reverse_lazy('view_setup'))
|
||||
return HttpResponseRedirect(reverse_lazy('view_search'))
|
||||
try:
|
||||
@ -39,14 +44,19 @@ def index(request):
|
||||
UserPreference.BOOKS: reverse_lazy('view_books'),
|
||||
}
|
||||
|
||||
return HttpResponseRedirect(page_map.get(request.user.userpreference.default_page))
|
||||
return HttpResponseRedirect(
|
||||
page_map.get(request.user.userpreference.default_page)
|
||||
)
|
||||
except UserPreference.DoesNotExist:
|
||||
return HttpResponseRedirect(reverse('login') + '?next=' + request.path)
|
||||
|
||||
|
||||
def search(request):
|
||||
if request.user.is_authenticated:
|
||||
f = RecipeFilter(request.GET, queryset=Recipe.objects.all().order_by('name'))
|
||||
f = RecipeFilter(
|
||||
request.GET,
|
||||
queryset=Recipe.objects.all().order_by('name')
|
||||
)
|
||||
|
||||
if request.user.userpreference.search_style == UserPreference.LARGE:
|
||||
table = RecipeTable(f.qs)
|
||||
@ -55,7 +65,10 @@ def search(request):
|
||||
RequestConfig(request, paginate={'per_page': 25}).configure(table)
|
||||
|
||||
if request.GET == {} and request.user.userpreference.show_recent:
|
||||
qs = Recipe.objects.filter(viewlog__created_by=request.user).order_by('-viewlog__created_at').all()
|
||||
qs = Recipe.objects \
|
||||
.filter(viewlog__created_by=request.user) \
|
||||
.order_by('-viewlog__created_at') \
|
||||
.all()
|
||||
|
||||
recent_list = []
|
||||
for r in qs:
|
||||
@ -68,7 +81,11 @@ def search(request):
|
||||
else:
|
||||
last_viewed = None
|
||||
|
||||
return render(request, 'index.html', {'recipes': table, 'filter': f, 'last_viewed': last_viewed})
|
||||
return render(
|
||||
request,
|
||||
'index.html',
|
||||
{'recipes': table, 'filter': f, 'last_viewed': last_viewed}
|
||||
)
|
||||
else:
|
||||
return HttpResponseRedirect(reverse('login') + '?next=' + request.path)
|
||||
|
||||
@ -77,16 +94,28 @@ def recipe_view(request, pk, share=None):
|
||||
recipe = get_object_or_404(Recipe, pk=pk)
|
||||
|
||||
if not request.user.is_authenticated and not share_link_valid(recipe, share):
|
||||
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.ERROR,
|
||||
_('You do not have the required permissions to view this page!')
|
||||
)
|
||||
return HttpResponseRedirect(reverse('login') + '?next=' + request.path)
|
||||
|
||||
comments = Comment.objects.filter(recipe=recipe)
|
||||
|
||||
if request.method == "POST":
|
||||
if not request.user.is_authenticated:
|
||||
messages.add_message(request, messages.ERROR,
|
||||
_('You do not have the required permissions to perform this action!'))
|
||||
return HttpResponseRedirect(reverse('view_recipe', kwargs={'pk': recipe.pk, 'share': share}))
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.ERROR,
|
||||
_('You do not have the required permissions to perform this action!') # noqa: E501
|
||||
)
|
||||
return HttpResponseRedirect(
|
||||
reverse(
|
||||
'view_recipe',
|
||||
kwargs={'pk': recipe.pk, 'share': share}
|
||||
)
|
||||
)
|
||||
|
||||
comment_form = CommentForm(request.POST, prefix='comment')
|
||||
if comment_form.is_valid():
|
||||
@ -97,7 +126,9 @@ def recipe_view(request, pk, share=None):
|
||||
|
||||
comment.save()
|
||||
|
||||
messages.add_message(request, messages.SUCCESS, _('Comment saved!'))
|
||||
messages.add_message(
|
||||
request, messages.SUCCESS, _('Comment saved!')
|
||||
)
|
||||
|
||||
bookmark_form = RecipeBookEntryForm(request.POST, prefix='bookmark')
|
||||
if bookmark_form.is_valid():
|
||||
@ -105,42 +136,79 @@ def recipe_view(request, pk, share=None):
|
||||
bookmark.recipe = recipe
|
||||
bookmark.book = bookmark_form.cleaned_data['book']
|
||||
|
||||
try:
|
||||
bookmark.save()
|
||||
|
||||
messages.add_message(request, messages.SUCCESS, _('Bookmark saved!'))
|
||||
except IntegrityError as e:
|
||||
if 'UNIQUE constraint' in str(e.args):
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.ERROR,
|
||||
_('This recipe is already linked to the book!')
|
||||
)
|
||||
else:
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.SUCCESS,
|
||||
_('Bookmark saved!')
|
||||
)
|
||||
|
||||
comment_form = CommentForm()
|
||||
bookmark_form = RecipeBookEntryForm()
|
||||
|
||||
user_servings = None
|
||||
if request.user.is_authenticated:
|
||||
user_servings = CookLog.objects.filter(recipe=recipe, created_by=request.user,
|
||||
servings__gt=0).all().aggregate(Avg('servings'))['servings__avg']
|
||||
user_servings = CookLog.objects.filter(
|
||||
recipe=recipe,
|
||||
created_by=request.user,
|
||||
servings__gt=0
|
||||
).all().aggregate(Avg('servings'))['servings__avg']
|
||||
|
||||
if request.user.is_authenticated:
|
||||
if not ViewLog.objects.filter(recipe=recipe).filter(created_by=request.user).filter(
|
||||
created_at__gt=(timezone.now() - timezone.timedelta(minutes=5))).exists():
|
||||
if not ViewLog.objects \
|
||||
.filter(recipe=recipe) \
|
||||
.filter(created_by=request.user) \
|
||||
.filter(created_at__gt=(
|
||||
timezone.now() - timezone.timedelta(minutes=5))) \
|
||||
.exists():
|
||||
ViewLog.objects.create(recipe=recipe, created_by=request.user)
|
||||
|
||||
return render(request, 'recipe_view.html',
|
||||
{'recipe': recipe, 'comments': comments, 'comment_form': comment_form,
|
||||
'bookmark_form': bookmark_form, 'share': share, 'user_servings': user_servings})
|
||||
return render(
|
||||
request,
|
||||
'recipe_view.html',
|
||||
{
|
||||
'recipe': recipe,
|
||||
'comments': comments,
|
||||
'comment_form': comment_form,
|
||||
'bookmark_form': bookmark_form,
|
||||
'share': share,
|
||||
'user_servings': user_servings
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@group_required('user')
|
||||
def books(request):
|
||||
book_list = []
|
||||
|
||||
books = RecipeBook.objects.filter(Q(created_by=request.user) | Q(shared=request.user)).distinct().all()
|
||||
books = RecipeBook.objects.filter(
|
||||
Q(created_by=request.user) | Q(shared=request.user)
|
||||
).distinct().all()
|
||||
|
||||
for b in books:
|
||||
book_list.append({'book': b, 'recipes': RecipeBookEntry.objects.filter(book=b).all()})
|
||||
book_list.append(
|
||||
{
|
||||
'book': b,
|
||||
'recipes': RecipeBookEntry.objects.filter(book=b).all()
|
||||
}
|
||||
)
|
||||
|
||||
return render(request, 'books.html', {'book_list': book_list})
|
||||
|
||||
|
||||
def get_start_end_from_week(p_year, p_week):
|
||||
first_day_of_week = datetime.strptime(f'{p_year}-W{int(p_week) - 1}-1', "%Y-W%W-%w").date()
|
||||
first_day_of_week = datetime.strptime(
|
||||
f'{p_year}-W{int(p_week) - 1}-1', "%Y-W%W-%w"
|
||||
).date()
|
||||
last_day_of_week = first_day_of_week + timedelta(days=6.9)
|
||||
return first_day_of_week, last_day_of_week
|
||||
|
||||
@ -163,13 +231,24 @@ def meal_plan_entry(request, pk):
|
||||
plan = MealPlan.objects.get(pk=pk)
|
||||
|
||||
if plan.created_by != request.user and plan.shared != request.user:
|
||||
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.ERROR,
|
||||
_('You do not have the required permissions to view this page!')
|
||||
)
|
||||
return HttpResponseRedirect(reverse_lazy('index'))
|
||||
|
||||
same_day_plan = MealPlan.objects.filter(date=plan.date).exclude(pk=plan.pk).filter(
|
||||
Q(created_by=request.user) | Q(shared=request.user)).order_by('meal_type').all()
|
||||
same_day_plan = MealPlan.objects \
|
||||
.filter(date=plan.date) \
|
||||
.exclude(pk=plan.pk) \
|
||||
.filter(Q(created_by=request.user) | Q(shared=request.user)) \
|
||||
.order_by('meal_type').all()
|
||||
|
||||
return render(request, 'meal_plan_entry.html', {'plan': plan, 'same_day_plan': same_day_plan})
|
||||
return render(
|
||||
request,
|
||||
'meal_plan_entry.html',
|
||||
{'plan': plan, 'same_day_plan': same_day_plan}
|
||||
)
|
||||
|
||||
|
||||
@group_required('user')
|
||||
@ -184,7 +263,11 @@ def shopping_list(request, pk=None):
|
||||
if recipe := Recipe.objects.filter(pk=int(rid)).first():
|
||||
recipes.append({'recipe': recipe.id, 'multiplier': multiplier})
|
||||
|
||||
return render(request, 'shopping_list.html', {'shopping_list_id': pk, 'recipes': recipes})
|
||||
return render(
|
||||
request,
|
||||
'shopping_list.html',
|
||||
{'shopping_list_id': pk, 'recipes': recipes}
|
||||
)
|
||||
|
||||
|
||||
@group_required('guest')
|
||||
@ -209,22 +292,22 @@ def user_settings(request):
|
||||
up.show_recent = form.cleaned_data['show_recent']
|
||||
up.search_style = form.cleaned_data['search_style']
|
||||
up.plan_share.set(form.cleaned_data['plan_share'])
|
||||
up.ingredient_decimals = form.cleaned_data['ingredient_decimals']
|
||||
up.ingredient_decimals = form.cleaned_data['ingredient_decimals'] # noqa: E501
|
||||
up.comments = form.cleaned_data['comments']
|
||||
up.use_fractions = form.cleaned_data['use_fractions']
|
||||
up.sticky_navbar = form.cleaned_data['sticky_navbar']
|
||||
|
||||
up.shopping_auto_sync = form.cleaned_data['shopping_auto_sync']
|
||||
if up.shopping_auto_sync < settings.SHOPPING_MIN_AUTOSYNC_INTERVAL:
|
||||
up.shopping_auto_sync = settings.SHOPPING_MIN_AUTOSYNC_INTERVAL
|
||||
if up.shopping_auto_sync < settings.SHOPPING_MIN_AUTOSYNC_INTERVAL: # noqa: E501
|
||||
up.shopping_auto_sync = settings.SHOPPING_MIN_AUTOSYNC_INTERVAL # noqa: E501
|
||||
|
||||
up.save()
|
||||
|
||||
if 'user_name_form' in request.POST:
|
||||
user_name_form = UserNameForm(request.POST, prefix='name')
|
||||
if user_name_form.is_valid():
|
||||
request.user.first_name = user_name_form.cleaned_data['first_name']
|
||||
request.user.last_name = user_name_form.cleaned_data['last_name']
|
||||
request.user.first_name = user_name_form.cleaned_data['first_name'] # noqa: E501
|
||||
request.user.last_name = user_name_form.cleaned_data['last_name'] # noqa: E501
|
||||
request.user.save()
|
||||
|
||||
if 'password_form' in request.POST:
|
||||
@ -241,40 +324,74 @@ def user_settings(request):
|
||||
if (api_token := Token.objects.filter(user=request.user).first()) is None:
|
||||
api_token = Token.objects.create(user=request.user)
|
||||
|
||||
return render(request, 'settings.html',
|
||||
{'preference_form': preference_form, 'user_name_form': user_name_form, 'password_form': password_form,
|
||||
'api_token': api_token})
|
||||
return render(
|
||||
request,
|
||||
'settings.html',
|
||||
{
|
||||
'preference_form': preference_form,
|
||||
'user_name_form': user_name_form,
|
||||
'password_form': password_form,
|
||||
'api_token': api_token
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@group_required('guest')
|
||||
def history(request):
|
||||
view_log = ViewLogTable(ViewLog.objects.filter(created_by=request.user).order_by('-created_at').all())
|
||||
cook_log = CookLogTable(CookLog.objects.filter(created_by=request.user).order_by('-created_at').all())
|
||||
return render(request, 'history.html', {'view_log': view_log, 'cook_log': cook_log})
|
||||
view_log = ViewLogTable(
|
||||
ViewLog.objects.filter(
|
||||
created_by=request.user
|
||||
).order_by('-created_at').all()
|
||||
)
|
||||
cook_log = CookLogTable(
|
||||
CookLog.objects.filter(
|
||||
created_by=request.user
|
||||
).order_by('-created_at').all()
|
||||
)
|
||||
return render(
|
||||
request,
|
||||
'history.html',
|
||||
{'view_log': view_log, 'cook_log': cook_log}
|
||||
)
|
||||
|
||||
|
||||
@group_required('admin')
|
||||
def system(request):
|
||||
postgres = False if (settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql_psycopg2' or
|
||||
settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql') else True
|
||||
postgres = False if (
|
||||
settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql_psycopg2' # noqa: E501
|
||||
or settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql' # noqa: E501
|
||||
) else True
|
||||
|
||||
secret_key = False if os.getenv('SECRET_KEY') else True
|
||||
|
||||
return render(request, 'system.html',
|
||||
{'gunicorn_media': settings.GUNICORN_MEDIA, 'debug': settings.DEBUG, 'postgres': postgres,
|
||||
'version': VERSION_NUMBER, 'ref': BUILD_REF, 'secret_key': secret_key})
|
||||
return render(
|
||||
request,
|
||||
'system.html',
|
||||
{
|
||||
'gunicorn_media': settings.GUNICORN_MEDIA,
|
||||
'debug': settings.DEBUG,
|
||||
'postgres': postgres,
|
||||
'version': VERSION_NUMBER,
|
||||
'ref': BUILD_REF,
|
||||
'secret_key': secret_key
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def setup(request):
|
||||
if User.objects.count() > 0 or 'django.contrib.auth.backends.RemoteUserBackend' in settings.AUTHENTICATION_BACKENDS:
|
||||
messages.add_message(request, messages.ERROR, _(
|
||||
'The setup page can only be used to create the first user! If you have forgotten your superuser credentials please consult the django documentation on how to reset passwords.'))
|
||||
if (User.objects.count() > 0
|
||||
or 'django.contrib.auth.backends.RemoteUserBackend' in settings.AUTHENTICATION_BACKENDS): # noqa: E501
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.ERROR,
|
||||
_('The setup page can only be used to create the first user! If you have forgotten your superuser credentials please consult the django documentation on how to reset passwords.') # noqa: E501
|
||||
)
|
||||
return HttpResponseRedirect(reverse('login'))
|
||||
|
||||
if request.method == 'POST':
|
||||
form = UserCreateForm(request.POST)
|
||||
if form.is_valid():
|
||||
if form.cleaned_data['password'] != form.cleaned_data['password_confirm']:
|
||||
if form.cleaned_data['password'] != form.cleaned_data['password_confirm']: # noqa: E501
|
||||
form.add_error('password', _('Passwords dont match!'))
|
||||
else:
|
||||
user = User(
|
||||
@ -286,7 +403,11 @@ def setup(request):
|
||||
validate_password(form.cleaned_data['password'], user=user)
|
||||
user.set_password(form.cleaned_data['password'])
|
||||
user.save()
|
||||
messages.add_message(request, messages.SUCCESS, _('User has been created, please login!'))
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.SUCCESS,
|
||||
_('User has been created, please login!')
|
||||
)
|
||||
return HttpResponseRedirect(reverse('login'))
|
||||
except ValidationError as e:
|
||||
for m in e:
|
||||
@ -301,10 +422,14 @@ def signup(request, token):
|
||||
try:
|
||||
token = UUID(token, version=4)
|
||||
except ValueError:
|
||||
messages.add_message(request, messages.ERROR, _('Malformed Invite Link supplied!'))
|
||||
messages.add_message(
|
||||
request, messages.ERROR, _('Malformed Invite Link supplied!')
|
||||
)
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
|
||||
if link := InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None, uuid=token).first():
|
||||
if link := InviteLink.objects.filter(
|
||||
valid_until__gte=datetime.today(), used_by=None, uuid=token) \
|
||||
.first():
|
||||
if request.method == 'POST':
|
||||
|
||||
form = UserCreateForm(request.POST)
|
||||
@ -314,17 +439,23 @@ def signup(request, token):
|
||||
form.data = data
|
||||
|
||||
if form.is_valid():
|
||||
if form.cleaned_data['password'] != form.cleaned_data['password_confirm']:
|
||||
if form.cleaned_data['password'] != form.cleaned_data['password_confirm']: # noqa: E501
|
||||
form.add_error('password', _('Passwords dont match!'))
|
||||
else:
|
||||
user = User(
|
||||
username=form.cleaned_data['name'],
|
||||
)
|
||||
try:
|
||||
validate_password(form.cleaned_data['password'], user=user)
|
||||
validate_password(
|
||||
form.cleaned_data['password'], user=user
|
||||
)
|
||||
user.set_password(form.cleaned_data['password'])
|
||||
user.save()
|
||||
messages.add_message(request, messages.SUCCESS, _('User has been created, please login!'))
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.SUCCESS,
|
||||
_('User has been created, please login!')
|
||||
)
|
||||
|
||||
link.used_by = user
|
||||
link.save()
|
||||
@ -339,9 +470,13 @@ def signup(request, token):
|
||||
if link.username != '':
|
||||
form.fields['name'].initial = link.username
|
||||
form.fields['name'].disabled = True
|
||||
return render(request, 'registration/signup.html', {'form': form, 'link': link})
|
||||
return render(
|
||||
request, 'registration/signup.html', {'form': form, 'link': link}
|
||||
)
|
||||
|
||||
messages.add_message(request, messages.ERROR, _('Invite Link not valid or already used!'))
|
||||
messages.add_message(
|
||||
request, messages.ERROR, _('Invite Link not valid or already used!')
|
||||
)
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
|
||||
|
||||
@ -354,6 +489,10 @@ def api_info(request):
|
||||
return render(request, 'api_info.html', {})
|
||||
|
||||
|
||||
def offline(request):
|
||||
return render(request, 'offline.html', {})
|
||||
|
||||
|
||||
def test(request):
|
||||
if not settings.DEBUG:
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
|
@ -162,7 +162,7 @@ networks:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
nginx:
|
||||
nginx_config:
|
||||
staticfiles:
|
||||
```
|
||||
|
||||
@ -233,7 +233,7 @@ networks:
|
||||
name: nginx-proxy
|
||||
|
||||
volumes:
|
||||
nginx:
|
||||
nginx_config:
|
||||
staticfiles:
|
||||
```
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
from django.contrib.auth.middleware import RemoteUserMiddleware
|
||||
from os import getenv
|
||||
|
||||
from django.contrib.auth.middleware import RemoteUserMiddleware
|
||||
|
||||
|
||||
class CustomRemoteUser(RemoteUserMiddleware):
|
||||
header = getenv('PROXY_HEADER', 'HTTP_REMOTE_USER')
|
||||
|
@ -14,8 +14,8 @@ import random
|
||||
import string
|
||||
|
||||
from django.contrib import messages
|
||||
from dotenv import load_dotenv
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from dotenv import load_dotenv
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""recipes URL Configuration
|
||||
"""
|
||||
recipes URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/2.0/topics/http/urls/
|
||||
@ -15,9 +16,8 @@ Including another URLconf
|
||||
"""
|
||||
from django.conf import settings
|
||||
from django.conf.urls import url
|
||||
from django.conf.urls.static import static
|
||||
from django.urls import include, path
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
from django.views.i18n import JavaScriptCatalog
|
||||
from django.views.static import serve
|
||||
|
||||
@ -26,8 +26,16 @@ urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('accounts/', include('django.contrib.auth.urls')),
|
||||
path('i18n/', include('django.conf.urls.i18n')),
|
||||
path('jsi18n/', JavaScriptCatalog.as_view(domain='django'), name='javascript-catalog'),
|
||||
path(
|
||||
'jsi18n/',
|
||||
JavaScriptCatalog.as_view(domain='django'),
|
||||
name='javascript-catalog'
|
||||
),
|
||||
]
|
||||
|
||||
if settings.GUNICORN_MEDIA or settings.DEBUG:
|
||||
urlpatterns += url(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
|
||||
urlpatterns += url(
|
||||
r'^media/(?P<path>.*)$',
|
||||
serve,
|
||||
{'document_root': settings.MEDIA_ROOT}
|
||||
),
|
||||
|
@ -7,7 +7,7 @@ django-cleanup==5.1.0
|
||||
django-crispy-forms==1.10.0
|
||||
django-emoji-picker==0.0.6
|
||||
django-filter==2.4.0
|
||||
django-tables2==2.3.3
|
||||
django-tables2==2.3.4
|
||||
djangorestframework==3.12.2
|
||||
drf-writable-nested==0.6.2
|
||||
gunicorn==20.0.4
|
||||
|
Loading…
Reference in New Issue
Block a user