diff --git a/cookbook/admin.py b/cookbook/admin.py
index 30522ffe9..c9d443076 100644
--- a/cookbook/admin.py
+++ b/cookbook/admin.py
@@ -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)
diff --git a/cookbook/filters.py b/cookbook/filters.py
index 88e4c94b7..edfaa7dae 100644
--- a/cookbook/filters.py
+++ b/cookbook/filters.py
@@ -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
diff --git a/cookbook/forms.py b/cookbook/forms.py
index 5015e0c23..663106329 100644
--- a/cookbook/forms.py
+++ b/cookbook/forms.py
@@ -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 - [ ] in list for easier usage in markdown based documents.'),
+ help_text=_('Include - [ ] 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'}),
- 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'}),
- required=False,
- help_text=_('Leave empty for nextcloud and enter api token for dropbox.'))
+ 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.') # 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.') # 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 (/remote.php/webdav/ is added automatically)'),
+ 'url': _('Leave empty for dropbox and enter only base url for nextcloud (/remote.php/webdav/ 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 docs here')
+ '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 docs here') # 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'}
+ )
+ )
diff --git a/cookbook/helper/__init__.py b/cookbook/helper/__init__.py
index 824c1c590..4f07006da 100644
--- a/cookbook/helper/__init__.py
+++ b/cookbook/helper/__init__.py
@@ -1 +1,5 @@
-from cookbook.helper.dal import *
+import cookbook.helper.dal
+
+__all__ = [
+ 'dal',
+]
diff --git a/cookbook/helper/dal.py b/cookbook/helper/dal.py
index 3d5e85e62..03d8ed3ce 100644
--- a/cookbook/helper/dal.py
+++ b/cookbook/helper/dal.py
@@ -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
diff --git a/cookbook/helper/ingredient_parser.py b/cookbook/helper/ingredient_parser.py
index bbadf1b0c..2cb0e5dad 100644
--- a/cookbook/helper/ingredient_parser.py
+++ b/cookbook/helper/ingredient_parser.py
@@ -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:])
diff --git a/cookbook/helper/mdx_attributes.py b/cookbook/helper/mdx_attributes.py
index 0d3cb1bb9..389b2d264 100644
--- a/cookbook/helper/mdx_attributes.py
+++ b/cookbook/helper/mdx_attributes.py
@@ -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
+ )
diff --git a/cookbook/helper/mdx_urlize.py b/cookbook/helper/mdx_urlize.py
index 7df06430b..92bcc98be 100644
--- a/cookbook/helper/mdx_urlize.py
+++ b/cookbook/helper/mdx_urlize.py
@@ -1,4 +1,5 @@
-"""A more liberal autolinker
+"""
+A more liberal autolinker
Inspired by Django's urlize function.
@@ -45,27 +46,30 @@ 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)
-
+
if url.startswith('<'):
url = url[1:-1]
-
+
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
-
+
el = markdown.util.etree.Element("a")
el.set('href', url)
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()
diff --git a/cookbook/helper/permission_config.py b/cookbook/helper/permission_config.py
index 304f4bfc9..f06326ddd 100644
--- a/cookbook/helper/permission_config.py
+++ b/cookbook/helper/permission_config.py
@@ -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:
diff --git a/cookbook/helper/permission_helper.py b/cookbook/helper/permission_helper.py
index 9970bfff3..1b71ab2b4 100644
--- a/cookbook/helper/permission_helper.py
+++ b/cookbook/helper/permission_helper.py
@@ -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
diff --git a/cookbook/helper/recipe_url_import.py b/cookbook/helper/recipe_url_import.py
index 49f6b05f0..4a9c7f0d7 100644
--- a/cookbook/helper/recipe_url_import.py
+++ b/cookbook/helper/recipe_url_import.py
@@ -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('
', '') - ld_json['recipeInstructions'] = ld_json['recipeInstructions'].replace('
', '') + 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('', '') # noqa: E501 + ld_json['recipeInstructions'] = ld_json['recipeInstructions'].replace('
', '') # 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) diff --git a/cookbook/helper/template_helper.py b/cookbook/helper/template_helper.py index 358e9e81b..48c6aa9f1 100644 --- a/cookbook/helper/template_helper.py +++ b/cookbook/helper/template_helper.py @@ -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) diff --git a/cookbook/models.py b/cookbook/models.py index 6f88069c0..7f5ae9749 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -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): @@ -279,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() @@ -301,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): @@ -315,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) @@ -337,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) @@ -361,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) diff --git a/cookbook/provider/dropbox.py b/cookbook/provider/dropbox.py index acdfe6510..2c81c9056 100644 --- a/cookbook/provider/dropbox.py +++ b/cookbook/provider/dropbox.py @@ -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)) diff --git a/cookbook/provider/nextcloud.py b/cookbook/provider/nextcloud.py index 10922dd18..e63c4f706 100644 --- a/cookbook/provider/nextcloud.py +++ b/cookbook/provider/nextcloud.py @@ -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 diff --git a/cookbook/serializer.py b/cookbook/serializer.py index 8992627c0..9c8375acd 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -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',) diff --git a/cookbook/tables.py b/cookbook/tables.py index a29a75366..a5c1cca82 100644 --- a/cookbook/tables.py +++ b/cookbook/tables.py @@ -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("" + _('Edit') + "") + edit = tables.TemplateColumn( + "" + _('Edit') + "" # 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('%s' % value) + return format_html( + '%s' % value + ) else: - return format_html('%s' % value) + return format_html( + '%s' % value + ) class Meta: model = SyncLog @@ -90,7 +108,9 @@ class SyncTable(tables.Table): @staticmethod def render_storage(value): - return format_html('%s' % value) + return format_html( + '%s' % 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("" + _('Delete') + "") + delete = tables.TemplateColumn( + "" + _('Delete') + "" # noqa: E501 + ) class Meta: model = RecipeImport @@ -118,13 +140,19 @@ class ShoppingListTable(tables.Table): class InviteLinkTable(tables.Table): - link = tables.TemplateColumn("" + _('Link') + "") - delete = tables.TemplateColumn("" + _('Delete') + "") + link = tables.TemplateColumn( + "" + _('Link') + "" + ) + delete = tables.TemplateColumn( + "" + _('Delete') + "" # 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): diff --git a/cookbook/templatetags/__init__.py b/cookbook/templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cookbook/templatetags/custom_tags.py b/cookbook/templatetags/custom_tags.py index 147d505ab..fd1b9abc8 100644 --- a/cookbook/templatetags/custom_tags.py +++ b/cookbook/templatetags/custom_tags.py @@ -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 = '' @@ -51,7 +60,7 @@ def recipe_rating(recipe, user): rating_stars = rating_stars + '' if rating['rating__avg'] % 1 >= 0.5: - rating_stars = rating_stars + '' + rating_stars = rating_stars + '' # noqa: E501 rating_stars += '' diff --git a/cookbook/templatetags/theming_tags.py b/cookbook/templatetags/theming_tags.py index 9172c9b67..8da775c24 100644 --- a/cookbook/templatetags/theming_tags.py +++ b/cookbook/templatetags/theming_tags.py @@ -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 '' diff --git a/cookbook/tests/api/test_api_food.py b/cookbook/tests/api/test_api_food.py index 87e3317c0..1f0cf5192 100644 --- a/cookbook/tests/api/test_api_food.py +++ b/cookbook/tests/api/test_api_food.py @@ -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) diff --git a/cookbook/tests/api/test_api_keyword.py b/cookbook/tests/api/test_api_keyword.py index 90fb68c30..a86f4e426 100644 --- a/cookbook/tests/api/test_api_keyword.py +++ b/cookbook/tests/api/test_api_keyword.py @@ -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) diff --git a/cookbook/tests/api/test_api_recipe.py b/cookbook/tests/api/test_api_recipe.py index a6ddfa5b2..9bd679011 100644 --- a/cookbook/tests/api/test_api_recipe.py +++ b/cookbook/tests/api/test_api_recipe.py @@ -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 diff --git a/cookbook/tests/api/test_api_shopping.py b/cookbook/tests/api/test_api_shopping.py index cb902bb4d..e25a16a5a 100644 --- a/cookbook/tests/api/test_api_shopping.py +++ b/cookbook/tests/api/test_api_shopping.py @@ -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 diff --git a/cookbook/tests/api/test_api_storage.py b/cookbook/tests/api/test_api_storage.py index 67300f48a..64f57ff84 100644 --- a/cookbook/tests/api/test_api_storage.py +++ b/cookbook/tests/api/test_api_storage.py @@ -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}) + ) diff --git a/cookbook/tests/api/test_api_syn_log.py b/cookbook/tests/api/test_api_syn_log.py index d899b28ef..0efaaabdb 100644 --- a/cookbook/tests/api/test_api_syn_log.py +++ b/cookbook/tests/api/test_api_syn_log.py @@ -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) diff --git a/cookbook/tests/api/test_api_sync.py b/cookbook/tests/api/test_api_sync.py index 1b5963cd7..26c84df15 100644 --- a/cookbook/tests/api/test_api_sync.py +++ b/cookbook/tests/api/test_api_sync.py @@ -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) diff --git a/cookbook/tests/api/test_api_unit.py b/cookbook/tests/api/test_api_unit.py index 3d849166b..e21a4f34d 100644 --- a/cookbook/tests/api/test_api_unit.py +++ b/cookbook/tests/api/test_api_unit.py @@ -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) diff --git a/cookbook/tests/api/test_api_username.py b/cookbook/tests/api/test_api_username.py index 23f3c2732..23657ece2 100644 --- a/cookbook/tests/api/test_api_username.py +++ b/cookbook/tests/api/test_api_username.py @@ -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') + ) diff --git a/cookbook/tests/api/test_api_userpreference.py b/cookbook/tests/api/test_api_userpreference.py index f13ae2745..7e8d051aa 100644 --- a/cookbook/tests/api/test_api_userpreference.py +++ b/cookbook/tests/api/test_api_userpreference.py @@ -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) diff --git a/cookbook/tests/edits/test_edits_comment.py b/cookbook/tests/edits/test_edits_comment.py index af3ff903f..0c8216f06 100644 --- a/cookbook/tests/edits/test_edits_comment.py +++ b/cookbook/tests/edits/test_edits_comment.py @@ -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): diff --git a/cookbook/tests/edits/test_edits_recipe.py b/cookbook/tests/edits/test_edits_recipe.py index 6a71292ac..da91ef3f8 100644 --- a/cookbook/tests/edits/test_edits_recipe.py +++ b/cookbook/tests/edits/test_edits_recipe.py @@ -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) diff --git a/cookbook/tests/edits/test_edits_storage.py b/cookbook/tests/edits/test_edits_storage.py index d1fa8fd9b..cb122e71c 100644 --- a/cookbook/tests/edits/test_edits_storage.py +++ b/cookbook/tests/edits/test_edits_storage.py @@ -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) diff --git a/cookbook/tests/other/test_edits_recipe.py b/cookbook/tests/other/test_edits_recipe.py index 0456eb6e9..46d9c9864 100644 --- a/cookbook/tests/other/test_edits_recipe.py +++ b/cookbook/tests/other/test_edits_recipe.py @@ -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 diff --git a/cookbook/tests/test_setup.py b/cookbook/tests/test_setup.py index f5447e207..7f9ba59c5 100644 --- a/cookbook/tests/test_setup.py +++ b/cookbook/tests/test_setup.py @@ -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 + ) diff --git a/cookbook/tests/views/test_views_api.py b/cookbook/tests/views/test_views_api.py index 39089d027..b3ae6bec7 100644 --- a/cookbook/tests/views/test_views_api.py +++ b/cookbook/tests/views/test_views_api.py @@ -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): diff --git a/cookbook/tests/views/test_views_general.py b/cookbook/tests/views/test_views_general.py index e99307fa3..69675ff91 100644 --- a/cookbook/tests/views/test_views_general.py +++ b/cookbook/tests/views/test_views_general.py @@ -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 + ) diff --git a/cookbook/tests/views/test_views_recipe_share.py b/cookbook/tests/views/test_views_recipe_share.py index 2c0575698..38b342dd5 100644 --- a/cookbook/tests/views/test_views_recipe_share.py +++ b/cookbook/tests/views/test_views_recipe_share.py @@ -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) diff --git a/cookbook/urls.py b/cookbook/urls.py index e1813836d..6d81a95ed 100644 --- a/cookbook/urls.py +++ b/cookbook/urls.py @@ -1,14 +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) @@ -43,44 +47,99 @@ urlpatterns = [ 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( + '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/