Merge branch 'develop' into feature/vue

This commit is contained in:
vabene1111 2021-01-11 10:04:55 +01:00
commit 5bb20bd479
61 changed files with 2102 additions and 662 deletions

View File

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

View File

@ -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

View File

@ -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'}
)
)

View File

@ -1 +1,5 @@
from cookbook.helper.dal import *
import cookbook.helper.dal
__all__ = [
'dal',
]

View File

@ -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

View File

@ -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:])

View File

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

View File

@ -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()

View File

@ -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:

View File

@ -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

View File

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

View File

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

View File

@ -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"

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

View File

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

View File

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

View File

@ -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

View File

@ -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',)

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View 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" }]
}
]
}

View File

@ -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):

View File

@ -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>

View 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 %}

View 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.
});

View File

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

View File

@ -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 ''

View File

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

View File

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

View File

@ -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

View File

@ -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

View File

@ -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})
)

View File

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

View File

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

View File

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

View File

@ -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')
)

View File

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

View File

@ -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):

View File

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

View File

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

View File

@ -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

View File

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

View File

@ -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):

View File

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

View File

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

View File

@ -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}'
)
)

View File

@ -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',
]

View File

@ -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

View File

@ -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

View File

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

View File

@ -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}
)

View File

@ -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')

View File

@ -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'
}
)

View File

@ -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

View File

@ -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'))

View File

@ -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:
```

View File

@ -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')

View File

@ -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__)))

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

View File

@ -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