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/', views.recipe_view, name='view_recipe'), - path('view/recipe//', views.recipe_view, name='view_recipe'), + path( + 'view/recipe//', + views.recipe_view, + name='view_recipe' + ), - path('new/recipe-import//', new.create_new_external_recipe, name='new_recipe_import'), + path( + 'new/recipe-import//', + new.create_new_external_recipe, + name='new_recipe_import' + ), path('new/share-link//', new.share_link, name='new_share_link'), path('edit/recipe//', edit.switch_recipe, name='edit_recipe'), - path('edit/recipe/internal//', edit.internal_recipe_update, name='edit_internal_recipe'), # for internal use only - path('edit/recipe/external//', edit.ExternalRecipeUpdate.as_view(), name='edit_external_recipe'), # for internal use only - path('edit/recipe/convert//', edit.convert_recipe, name='edit_convert_recipe'), # for internal use only + + # for internal use only + path( + 'edit/recipe/internal//', + edit.internal_recipe_update, + name='edit_internal_recipe' + ), + path( + 'edit/recipe/external//', + edit.ExternalRecipeUpdate.as_view(), + name='edit_external_recipe' + ), + path( + 'edit/recipe/convert//', + edit.convert_recipe, + name='edit_convert_recipe' + ), path('edit/storage//', edit.edit_storage, name='edit_storage'), path('edit/ingredient/', edit.edit_ingredients, name='edit_food'), - path('delete/recipe-source//', delete.delete_recipe_source, name='delete_recipe_source'), + path( + 'delete/recipe-source//', + delete.delete_recipe_source, + name='delete_recipe_source' + ), - path('data/sync', data.sync, name='data_sync'), # TODO move to generic "new" view + # TODO move to generic "new" view + path('data/sync', data.sync, name='data_sync'), path('data/batch/edit', data.batch_edit, name='data_batch_edit'), path('data/batch/import', data.batch_import, name='data_batch_import'), path('data/sync/wait', data.sync_wait, name='data_sync_wait'), path('data/statistics', data.statistics, name='data_stats'), path('data/import/url', data.import_url, name='data_import_url'), - path('api/get_external_file_link//', api.get_external_file_link, name='api_get_external_file_link'), - path('api/get_recipe_file//', api.get_recipe_file, name='api_get_recipe_file'), + path( + 'api/get_external_file_link//', + api.get_external_file_link, + name='api_get_external_file_link' + ), + path( + 'api/get_recipe_file//', + api.get_recipe_file, + name='api_get_recipe_file' + ), path('api/sync_all/', api.sync_all, name='api_sync'), - path('api/log_cooking//', api.log_cooking, name='api_log_cooking'), - path('api/plan-ical///', api.get_plan_ical, name='api_get_plan_ical'), - path('api/recipe-from-url/', api.recipe_from_url, name='api_recipe_from_url'), + path( + 'api/log_cooking//', + api.log_cooking, + name='api_log_cooking' + ), + path( + 'api/plan-ical///', + api.get_plan_ical, + name='api_get_plan_ical' + ), + path( + 'api/recipe-from-url/', api.recipe_from_url, name='api_recipe_from_url' + ), path('api/backup/', api.get_backup, name='api_backup'), - path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'), + path( + 'dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword' + ), path('dal/food/', dal.IngredientsAutocomplete.as_view(), name='dal_food'), path('dal/unit/', dal.UnitAutocomplete.as_view(), name='dal_unit'), @@ -93,24 +152,50 @@ urlpatterns = [ ), name='openapi-schema'), path('api/', include((router.urls, 'api'))), - path('api-auth/', include('rest_framework.urls', namespace='rest_framework')), + path( + 'api-auth/', + include('rest_framework.urls', namespace='rest_framework') + ), ] -generic_models = (Recipe, RecipeImport, Storage, RecipeBook, MealPlan, SyncLog, Sync, Comment, RecipeBookEntry, Keyword, Food, ShoppingList, InviteLink) +generic_models = ( + Recipe, RecipeImport, Storage, RecipeBook, MealPlan, SyncLog, Sync, + Comment, RecipeBookEntry, Keyword, Food, ShoppingList, InviteLink +) for m in generic_models: py_name = get_model_name(m) url_name = py_name.replace('_', '-') if c := locate(f'cookbook.views.new.{m.__name__}Create'): - urlpatterns.append(path(f'new/{url_name}/', c.as_view(), name=f'new_{py_name}')) + urlpatterns.append( + path( + f'new/{url_name}/', c.as_view(), name=f'new_{py_name}' + ) + ) if c := locate(f'cookbook.views.edit.{m.__name__}Update'): - urlpatterns.append(path(f'edit/{url_name}//', c.as_view(), name=f'edit_{py_name}')) + urlpatterns.append( + path( + f'edit/{url_name}//', + c.as_view(), + name=f'edit_{py_name}' + ) + ) if c := getattr(lists, py_name, None): - urlpatterns.append(path(f'list/{url_name}/', c, name=f'list_{py_name}')) + urlpatterns.append( + path( + f'list/{url_name}/', c, name=f'list_{py_name}' + ) + ) if c := locate(f'cookbook.views.delete.{m.__name__}Delete'): - urlpatterns.append(path(f'delete/{url_name}//', c.as_view(), name=f'delete_{py_name}')) + urlpatterns.append( + path( + f'delete/{url_name}//', + c.as_view(), + name=f'delete_{py_name}' + ) + ) diff --git a/cookbook/views/__init__.py b/cookbook/views/__init__.py index fe952811a..ab37dad90 100644 --- a/cookbook/views/__init__.py +++ b/cookbook/views/__init__.py @@ -1,7 +1,19 @@ -from cookbook.views.views import * -from cookbook.views.api import * -from cookbook.views.data import * -from cookbook.views.edit import * -from cookbook.views.new import * -from cookbook.views.lists import * -from cookbook.views.delete import * +import cookbook.views.api +import cookbook.views.data +import cookbook.views.delete +import cookbook.views.edit +import cookbook.views.import_export +import cookbook.views.lists +import cookbook.views.new +import cookbook.views.views + +__all__ = [ + 'api', + 'data', + 'delete', + 'edit', + 'import_export', + 'lists', + 'new', + 'views', +] diff --git a/cookbook/views/api.py b/cookbook/views/api.py index 45148929c..05a46d981 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -4,7 +4,6 @@ import re import uuid import requests -from PIL import Image from annoying.decorators import ajax_request from annoying.functions import get_object_or_None from django.contrib import messages @@ -12,28 +11,45 @@ from django.contrib.auth.models import User from django.core import management from django.core.files import File from django.db.models import Q -from django.http import HttpResponse, FileResponse, JsonResponse +from django.http import FileResponse, HttpResponse, JsonResponse from django.shortcuts import redirect -from django.utils import timezone, dateformat +from django.utils import timezone from django.utils.formats import date_format from django.utils.translation import gettext as _ -from django.views.generic.base import View from icalendar import Calendar, Event -from rest_framework import viewsets, permissions, decorators +from PIL import Image +from rest_framework import decorators, permissions, viewsets from rest_framework.exceptions import APIException -from rest_framework.mixins import RetrieveModelMixin, UpdateModelMixin, ListModelMixin -from rest_framework.parsers import JSONParser, FileUploadParser, MultiPartParser +from rest_framework.mixins import (ListModelMixin, RetrieveModelMixin, + UpdateModelMixin) +from rest_framework.parsers import MultiPartParser from rest_framework.response import Response from rest_framework.viewsets import ViewSetMixin -from cookbook.helper.permission_helper import group_required, CustomIsOwner, CustomIsAdmin, CustomIsUser, CustomIsGuest, CustomIsShare, CustomIsShared +from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest, + CustomIsOwner, CustomIsShare, + CustomIsShared, CustomIsUser, + group_required) from cookbook.helper.recipe_url_import import get_from_html -from cookbook.models import Recipe, Sync, Storage, CookLog, MealPlan, MealType, ViewLog, UserPreference, RecipeBook, Ingredient, Food, Step, Keyword, Unit, SyncLog, ShoppingListRecipe, ShoppingList, ShoppingListEntry +from cookbook.models import (CookLog, Food, Ingredient, Keyword, MealPlan, + MealType, Recipe, RecipeBook, ShoppingList, + ShoppingListEntry, ShoppingListRecipe, Step, + Storage, Sync, SyncLog, Unit, UserPreference, + ViewLog) from cookbook.provider.dropbox import Dropbox from cookbook.provider.nextcloud import Nextcloud -from cookbook.serializer import MealPlanSerializer, MealTypeSerializer, RecipeSerializer, ViewLogSerializer, UserNameSerializer, UserPreferenceSerializer, RecipeBookSerializer, IngredientSerializer, FoodSerializer, StepSerializer, \ - KeywordSerializer, RecipeImageSerializer, StorageSerializer, SyncSerializer, SyncLogSerializer, UnitSerializer, ShoppingListSerializer, ShoppingListRecipeSerializer, ShoppingListEntrySerializer, ShoppingListEntryCheckedSerializer, \ - ShoppingListAutoSyncSerializer +from cookbook.serializer import (FoodSerializer, IngredientSerializer, + KeywordSerializer, MealPlanSerializer, + MealTypeSerializer, RecipeBookSerializer, + RecipeImageSerializer, RecipeSerializer, + ShoppingListAutoSyncSerializer, + ShoppingListEntrySerializer, + ShoppingListRecipeSerializer, + ShoppingListSerializer, StepSerializer, + StorageSerializer, SyncLogSerializer, + SyncSerializer, UnitSerializer, + UserNameSerializer, UserPreferenceSerializer, + ViewLogSerializer) class UserNameViewSet(viewsets.ReadOnlyModelViewSet): @@ -54,8 +70,10 @@ class UserNameViewSet(viewsets.ReadOnlyModelViewSet): filter_list = self.request.query_params.get('filter_list', None) if filter_list is not None: queryset = queryset.filter(pk__in=json.loads(filter_list)) - except ValueError as e: - raise APIException(_('Parameter filter_list incorrectly formatted')) + except ValueError: + raise APIException( + _('Parameter filter_list incorrectly formatted') + ) return queryset @@ -118,7 +136,8 @@ class KeywordViewSet(viewsets.ModelViewSet, StandardFilterMixin): list: optional parameters - - **query**: search keywords for a string contained in the keyword name (case in-sensitive) + - **query**: search keywords for a string contained + in the keyword name (case in-sensitive) - **limit**: limits the amount of returned results """ queryset = Keyword.objects.all() @@ -138,7 +157,12 @@ class FoodViewSet(viewsets.ModelViewSet, StandardFilterMixin): permission_classes = [CustomIsUser] -class RecipeBookViewSet(RetrieveModelMixin, UpdateModelMixin, ListModelMixin, viewsets.GenericViewSet): +class RecipeBookViewSet( + RetrieveModelMixin, + UpdateModelMixin, + ListModelMixin, + viewsets.GenericViewSet +): queryset = RecipeBook.objects.all() serializer_class = RecipeBookSerializer permission_classes = [CustomIsOwner, CustomIsAdmin] @@ -163,7 +187,10 @@ class MealPlanViewSet(viewsets.ModelViewSet): permission_classes = [permissions.IsAuthenticated] # TODO fix permissions def get_queryset(self): - queryset = MealPlan.objects.filter(Q(created_by=self.request.user) | Q(shared=self.request.user)).distinct().all() + queryset = MealPlan.objects.filter( + Q(created_by=self.request.user) | + Q(shared=self.request.user) + ).distinct().all() from_date = self.request.query_params.get('from_date', None) if from_date is not None: @@ -177,15 +204,16 @@ class MealPlanViewSet(viewsets.ModelViewSet): class MealTypeViewSet(viewsets.ModelViewSet): """ - list: - returns list of meal types created by the requesting user ordered by the order field + returns list of meal types created by the + requesting user ordered by the order field. """ queryset = MealType.objects.order_by('order').all() serializer_class = MealTypeSerializer permission_classes = [permissions.IsAuthenticated] def get_queryset(self): - queryset = MealType.objects.order_by('order', 'id').filter(created_by=self.request.user).all() + queryset = MealType.objects.order_by('order', 'id') \ + .filter(created_by=self.request.user).all() return queryset @@ -206,12 +234,14 @@ class RecipeViewSet(viewsets.ModelViewSet, StandardFilterMixin): list: optional parameters - - **query**: search recipes for a string contained in the recipe name (case in-sensitive) + - **query**: search recipes for a string contained + in the recipe name (case in-sensitive) - **limit**: limits the amount of returned results """ queryset = Recipe.objects.all() serializer_class = RecipeSerializer - permission_classes = [CustomIsShare | CustomIsGuest] # TODO split read and write permission for meal plan guest + # TODO split read and write permission for meal plan guest + permission_classes = [CustomIsShare | CustomIsGuest] def get_queryset(self): @@ -231,7 +261,9 @@ class RecipeViewSet(viewsets.ModelViewSet, StandardFilterMixin): ) def image(self, request, pk): obj = self.get_object() - serializer = self.serializer_class(obj, data=request.data, partial=True) + serializer = self.serializer_class( + obj, data=request.data, partial=True + ) if serializer.is_valid(): serializer.save() @@ -275,7 +307,9 @@ class ShoppingListViewSet(viewsets.ModelViewSet): def get_queryset(self): if self.request.user.is_superuser: return self.queryset - return self.queryset.filter(Q(created_by=self.request.user) | Q(shared=self.request.user)).all() + return self.queryset.filter( + Q(created_by=self.request.user) | Q(shared=self.request.user) + ).all() def get_serializer_class(self): autosync = self.request.query_params.get('autosync', None) @@ -290,7 +324,8 @@ class ViewLogViewSet(viewsets.ModelViewSet): permission_classes = [permissions.IsAuthenticated] def get_queryset(self): - queryset = ViewLog.objects.filter(created_by=self.request.user).all()[:5] + queryset = ViewLog.objects \ + .filter(created_by=self.request.user).all()[:5] return queryset @@ -307,7 +342,8 @@ def get_recipe_provider(recipe): def update_recipe_links(recipe): if not recipe.link: - recipe.link = get_recipe_provider(recipe).get_share_link(recipe) # TODO response validation in apis + # TODO response validation in apis + recipe.link = get_recipe_provider(recipe).get_share_link(recipe) recipe.save() @@ -346,10 +382,14 @@ def sync_all(request): error = True if not error: - messages.add_message(request, messages.SUCCESS, _('Sync successful!')) + messages.add_message( + request, messages.SUCCESS, _('Sync successful!') + ) return redirect('list_recipe_import') else: - messages.add_message(request, messages.ERROR, _('Error synchronizing with Storage')) + messages.add_message( + request, messages.ERROR, _('Error synchronizing with Storage') + ) return redirect('list_recipe_import') @@ -374,7 +414,9 @@ def log_cooking(request, recipe_id): @group_required('user') def get_plan_ical(request, from_date, to_date): - queryset = MealPlan.objects.filter(Q(created_by=request.user) | Q(shared=request.user)).distinct().all() + queryset = MealPlan.objects.filter( + Q(created_by=request.user) | Q(shared=request.user) + ).distinct().all() if from_date is not None: queryset = queryset.filter(date__gte=from_date) @@ -394,7 +436,7 @@ def get_plan_ical(request, from_date, to_date): cal.add_component(event) response = FileResponse(io.BytesIO(cal.to_ical())) - response["Content-Disposition"] = f'attachment; filename=meal_plan_{from_date}-{to_date}.ics' + response["Content-Disposition"] = f'attachment; filename=meal_plan_{from_date}-{to_date}.ics' # noqa: E501 return response @@ -403,14 +445,28 @@ def get_plan_ical(request, from_date, to_date): def recipe_from_url(request): url = request.POST['url'] - headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36'} + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36' # noqa: E501 + } try: response = requests.get(url, headers=headers) except requests.exceptions.ConnectionError: - return JsonResponse({'error': True, 'msg': _('The requested page could not be found.')}, status=400) + return JsonResponse( + { + 'error': True, + 'msg': _('The requested page could not be found.') + }, + status=400 + ) if response.status_code == 403: - return JsonResponse({'error': True, 'msg': _('The requested page refused to provide any information (Status Code 403).')}, status=400) + return JsonResponse( + { + 'error': True, + 'msg': _('The requested page refused to provide any information (Status Code 403).') # noqa: E501 + }, + status=400 + ) return get_from_html(response.text, url) @@ -419,9 +475,11 @@ def get_backup(request): return HttpResponse('', status=403) buf = io.StringIO() - management.call_command('dumpdata', exclude=['contenttypes', 'auth'], stdout=buf) + management.call_command( + 'dumpdata', exclude=['contenttypes', 'auth'], stdout=buf + ) response = FileResponse(buf.getvalue()) - response["Content-Disposition"] = f'attachment; filename=backup{date_format(timezone.now(), format="SHORT_DATETIME_FORMAT", use_l10n=True)}.json' + response["Content-Disposition"] = f'attachment; filename=backup{date_format(timezone.now(), format="SHORT_DATETIME_FORMAT", use_l10n=True)}.json' # noqa: E501 return response diff --git a/cookbook/views/data.py b/cookbook/views/data.py index 01b788661..d55c674e9 100644 --- a/cookbook/views/data.py +++ b/cookbook/views/data.py @@ -1,22 +1,25 @@ import json +import uuid from datetime import datetime from io import BytesIO import requests -from PIL import Image, UnidentifiedImageError from django.contrib import messages from django.core.files import File from django.db.transaction import atomic -from django.utils.translation import gettext as _ -from django.http import HttpResponseRedirect, HttpResponse +from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import redirect, render from django.urls import reverse +from django.utils.translation import gettext as _ from django.utils.translation import ngettext from django_tables2 import RequestConfig +from PIL import Image, UnidentifiedImageError -from cookbook.forms import SyncForm, BatchEditForm -from cookbook.helper.permission_helper import group_required, has_group_permission -from cookbook.models import * +from cookbook.forms import BatchEditForm, SyncForm +from cookbook.helper.permission_helper import (group_required, + has_group_permission) +from cookbook.models import (Comment, Food, Ingredient, Keyword, Recipe, + RecipeImport, Step, Sync, Unit) from cookbook.tables import SyncTable @@ -24,7 +27,10 @@ from cookbook.tables import SyncTable def sync(request): if request.method == "POST": if not has_group_permission(request.user, ['admin']): - messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!')) + messages.add_message( + request, messages.ERROR, + _('You do not have the required permissions to view this page!') # noqa: E501 + ) return HttpResponseRedirect(reverse('data_sync')) form = SyncForm(request.POST) if form.is_valid(): @@ -38,9 +44,15 @@ def sync(request): form = SyncForm() monitored_paths = SyncTable(Sync.objects.all()) - RequestConfig(request, paginate={'per_page': 25}).configure(monitored_paths) + RequestConfig( + request, paginate={'per_page': 25} + ).configure(monitored_paths) - return render(request, 'batch/monitor.html', {'form': form, 'monitored_paths': monitored_paths}) + return render( + request, + 'batch/monitor.html', + {'form': form, 'monitored_paths': monitored_paths} + ) @group_required('user') @@ -52,7 +64,13 @@ def sync_wait(request): def batch_import(request): imports = RecipeImport.objects.all() for new_recipe in imports: - recipe = Recipe(name=new_recipe.name, file_path=new_recipe.file_path, storage=new_recipe.storage, file_uid=new_recipe.file_uid, created_by=request.user) + recipe = Recipe( + name=new_recipe.name, + file_path=new_recipe.file_path, + storage=new_recipe.storage, + file_uid=new_recipe.file_uid, + created_by=request.user + ) recipe.save() new_recipe.delete() @@ -115,7 +133,8 @@ def import_url(request): recipe.steps.add(step) for kw in data['keywords']: - if kw['id'] != "null" and (k := Keyword.objects.filter(id=kw['id']).first()): + if kw['id'] != "null" \ + and (k := Keyword.objects.filter(id=kw['id']).first()): recipe.keywords.add(k) elif data['all_keywords']: k = Keyword.objects.create(name=kw['text']) @@ -125,10 +144,14 @@ def import_url(request): ingredient = Ingredient() if ing['ingredient']['text'] != '': - ingredient.food, f_created = Food.objects.get_or_create(name=ing['ingredient']['text']) + ingredient.food, f_created = Food.objects.get_or_create( + name=ing['ingredient']['text'] + ) if ing['unit'] and ing['unit']['text'] != '': - ingredient.unit, u_created = Unit.objects.get_or_create(name=ing['unit']['text']) + ingredient.unit, u_created = Unit.objects.get_or_create( + name=ing['unit']['text'] + ) # TODO properly handle no_amount recipes if isinstance(ing['amount'], str): @@ -137,7 +160,8 @@ def import_url(request): except ValueError: ingredient.no_amount = True pass - elif isinstance(ing['amount'], float) or isinstance(ing['amount'], int): + elif isinstance(ing['amount'], float) \ + or isinstance(ing['amount'], int): ingredient.amount = ing['amount'] ingredient.note = ing['note'] if 'note' in ing else '' @@ -158,7 +182,9 @@ def import_url(request): im_io = BytesIO() img.save(im_io, 'PNG', quality=70) - recipe.image = File(im_io, name=f'{uuid.uuid4()}_{recipe.pk}.png') + recipe.image = File( + im_io, name=f'{uuid.uuid4()}_{recipe.pk}.png' + ) recipe.save() except UnidentifiedImageError: pass diff --git a/cookbook/views/delete.py b/cookbook/views/delete.py index 0a4df1cb9..a21427567 100644 --- a/cookbook/views/delete.py +++ b/cookbook/views/delete.py @@ -1,15 +1,17 @@ from django.contrib import messages -from django.contrib.auth.mixins import LoginRequiredMixin from django.db.models import ProtectedError from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404 -from django.urls import reverse_lazy, reverse +from django.urls import reverse, reverse_lazy from django.utils.translation import gettext as _ from django.views.generic import DeleteView -from cookbook.helper.permission_helper import group_required, GroupRequiredMixin, OwnerRequiredMixin -from cookbook.models import Recipe, Sync, Keyword, RecipeImport, Storage, Comment, RecipeBook, \ - RecipeBookEntry, MealPlan, Food, InviteLink +from cookbook.helper.permission_helper import (GroupRequiredMixin, + OwnerRequiredMixin, + group_required) +from cookbook.models import (Comment, InviteLink, Keyword, MealPlan, Recipe, + RecipeBook, RecipeBookEntry, RecipeImport, + Storage, Sync) from cookbook.provider.dropbox import Dropbox from cookbook.provider.nextcloud import Nextcloud @@ -31,7 +33,8 @@ def delete_recipe_source(request, pk): recipe = get_object_or_404(Recipe, pk=pk) if recipe.storage.method == Storage.DROPBOX: - Dropbox.delete_file(recipe) # TODO central location to handle storage type switches + # TODO central location to handle storage type switches + Dropbox.delete_file(recipe) if recipe.storage.method == Storage.NEXTCLOUD: Nextcloud.delete_file(recipe) @@ -94,7 +97,11 @@ class StorageDelete(GroupRequiredMixin, DeleteView): try: return self.delete(request, *args, **kwargs) except ProtectedError: - messages.add_message(request, messages.WARNING, _('Could not delete this storage backend as it is used in at least one monitor.')) + messages.add_message( + request, + messages.WARNING, + _('Could not delete this storage backend as it is used in at least one monitor.') # noqa: E501 + ) return HttpResponseRedirect(reverse('list_storage')) @@ -128,10 +135,16 @@ class RecipeBookEntryDelete(GroupRequiredMixin, DeleteView): def dispatch(self, request, *args, **kwargs): obj = self.get_object() - if not (obj.book.created_by == request.user or request.user.is_superuser): - messages.add_message(request, messages.ERROR, _('You cannot interact with this object as it is not owned by you!')) + if not (obj.book.created_by == request.user + or request.user.is_superuser): + messages.add_message( + request, + messages.ERROR, + _('You cannot interact with this object as it is not owned by you!') # noqa: E501 + ) return HttpResponseRedirect(reverse('index')) - return super(RecipeBookEntryDelete, self).dispatch(request, *args, **kwargs) + return super(RecipeBookEntryDelete, self) \ + .dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): context = super(RecipeBookEntryDelete, self).get_context_data(**kwargs) diff --git a/cookbook/views/edit.py b/cookbook/views/edit.py index ac856b2c0..17050d566 100644 --- a/cookbook/views/edit.py +++ b/cookbook/views/edit.py @@ -7,12 +7,16 @@ from django.urls import reverse from django.utils.translation import gettext as _ from django.views.generic import UpdateView -from cookbook.forms import ExternalRecipeForm, KeywordForm, StorageForm, SyncForm, CommentForm, \ - MealPlanForm, UnitMergeForm, RecipeBookForm, FoodForm, FoodMergeForm -from cookbook.helper.permission_helper import OwnerRequiredMixin -from cookbook.helper.permission_helper import group_required, GroupRequiredMixin -from cookbook.models import Recipe, Sync, Keyword, RecipeImport, Storage, Comment, Ingredient, RecipeBook, \ - MealPlan, Food, MealType +from cookbook.forms import (CommentForm, ExternalRecipeForm, FoodForm, + FoodMergeForm, KeywordForm, MealPlanForm, + RecipeBookForm, StorageForm, SyncForm, + UnitMergeForm) +from cookbook.helper.permission_helper import (GroupRequiredMixin, + OwnerRequiredMixin, + group_required) +from cookbook.models import (Comment, Food, Ingredient, Keyword, MealPlan, + MealType, Recipe, RecipeBook, RecipeImport, + Storage, Sync) from cookbook.provider.dropbox import Dropbox from cookbook.provider.nextcloud import Nextcloud @@ -40,7 +44,9 @@ def convert_recipe(request, pk): def internal_recipe_update(request, pk): recipe_instance = get_object_or_404(Recipe, pk=pk) - return render(request, 'forms/edit_internal_recipe.html', {'recipe': recipe_instance}) + return render( + request, 'forms/edit_internal_recipe.html', {'recipe': recipe_instance} + ) class SyncUpdate(GroupRequiredMixin, UpdateView): @@ -99,7 +105,9 @@ def edit_storage(request, pk): instance = get_object_or_404(Storage, pk=pk) if not (instance.created_by == request.user or request.user.is_superuser): - messages.add_message(request, messages.ERROR, _('You cannot edit this storage!')) + messages.add_message( + request, messages.ERROR, _('You cannot edit this storage!') + ) return HttpResponseRedirect(reverse('list_storage')) if request.method == "POST": @@ -118,16 +126,26 @@ def edit_storage(request, pk): instance.save() - messages.add_message(request, messages.SUCCESS, _('Storage saved!')) + messages.add_message( + request, messages.SUCCESS, _('Storage saved!') + ) else: - messages.add_message(request, messages.ERROR, _('There was an error updating this storage backend!')) + messages.add_message( + request, + messages.ERROR, + _('There was an error updating this storage backend!') + ) else: pseudo_instance = instance pseudo_instance.password = '__NO__CHANGE__' pseudo_instance.token = '__NO__CHANGE__' form = StorageForm(instance=pseudo_instance) - return render(request, 'generic/edit_template.html', {'form': form, 'title': _('Storage')}) + return render( + request, + 'generic/edit_template.html', + {'form': form, 'title': _('Storage')} + ) class CommentUpdate(OwnerRequiredMixin, UpdateView): @@ -141,7 +159,9 @@ class CommentUpdate(OwnerRequiredMixin, UpdateView): def get_context_data(self, **kwargs): context = super(CommentUpdate, self).get_context_data(**kwargs) context['title'] = _("Comment") - context['view_url'] = reverse('view_recipe', args=[self.object.recipe.pk]) + context['view_url'] = reverse( + 'view_recipe', args=[self.object.recipe.pk] + ) return context @@ -186,7 +206,8 @@ class MealPlanUpdate(OwnerRequiredMixin, UpdateView): def get_form(self, form_class=None): form = self.form_class(**self.get_form_kwargs()) - form.fields['meal_type'].queryset = MealType.objects.filter(created_by=self.request.user).all() + form.fields['meal_type'].queryset = MealType.objects \ + .filter(created_by=self.request.user).all() return form def get_context_data(self, **kwargs): @@ -206,17 +227,28 @@ class ExternalRecipeUpdate(GroupRequiredMixin, UpdateView): old_recipe = Recipe.objects.get(pk=self.object.pk) if not old_recipe.name == self.object.name: if self.object.storage.method == Storage.DROPBOX: - Dropbox.rename_file(old_recipe, self.object.name) # TODO central location to handle storage type switches + # TODO central location to handle storage type switches + Dropbox.rename_file(old_recipe, self.object.name) if self.object.storage.method == Storage.NEXTCLOUD: Nextcloud.rename_file(old_recipe, self.object.name) - self.object.file_path = os.path.dirname(self.object.file_path) + '/' + self.object.name + os.path.splitext(self.object.file_path)[1] + self.object.file_path = "%s/%s%s" % ( + os.path.dirname(self.object.file_path), + self.object.name, + os.path.splitext(self.object.file_path)[1] + ) - messages.add_message(self.request, messages.SUCCESS, _('Changes saved!')) + messages.add_message( + self.request, messages.SUCCESS, _('Changes saved!') + ) return super(ExternalRecipeUpdate, self).form_valid(form) def form_invalid(self, form): - messages.add_message(self.request, messages.ERROR, _('Error saving changes!')) + messages.add_message( + self.request, + messages.ERROR, + _('Error saving changes!') + ) return super(ExternalRecipeUpdate, self).form_valid(form) def get_success_url(self): @@ -227,7 +259,9 @@ class ExternalRecipeUpdate(GroupRequiredMixin, UpdateView): context['title'] = _("Recipe") context['view_url'] = reverse('view_recipe', args=[self.object.pk]) if self.object.storage: - context['delete_external_url'] = reverse('delete_recipe_source', args=[self.object.pk]) + context['delete_external_url'] = reverse( + 'delete_recipe_source', args=[self.object.pk] + ) return context @@ -240,16 +274,23 @@ def edit_ingredients(request): new_unit = units_form.cleaned_data['new_unit'] old_unit = units_form.cleaned_data['old_unit'] if new_unit != old_unit: - recipe_ingredients = Ingredient.objects.filter(unit=old_unit).all() + recipe_ingredients = Ingredient.objects \ + .filter(unit=old_unit).all() for i in recipe_ingredients: i.unit = new_unit i.save() old_unit.delete() success = True - messages.add_message(request, messages.SUCCESS, _('Units merged!')) + messages.add_message( + request, messages.SUCCESS, _('Units merged!') + ) else: - messages.add_message(request, messages.ERROR, _('Cannot merge with the same object!')) + messages.add_message( + request, + messages.ERROR, + _('Cannot merge with the same object!') + ) food_form = FoodMergeForm(request.POST, prefix=FoodMergeForm.prefix) if food_form.is_valid(): @@ -263,9 +304,15 @@ def edit_ingredients(request): old_food.delete() success = True - messages.add_message(request, messages.SUCCESS, _('Foods merged!')) + messages.add_message( + request, messages.SUCCESS, _('Foods merged!') + ) else: - messages.add_message(request, messages.ERROR, _('Cannot merge with the same object!')) + messages.add_message( + request, + messages.ERROR, + _('Cannot merge with the same object!') + ) if success: units_form = UnitMergeForm() @@ -274,4 +321,8 @@ def edit_ingredients(request): units_form = UnitMergeForm() food_form = FoodMergeForm() - return render(request, 'forms/ingredients.html', {'units_form': units_form, 'food_form': food_form}) + return render( + request, + 'forms/ingredients.html', + {'units_form': units_form, 'food_form': food_form} + ) diff --git a/cookbook/views/import_export.py b/cookbook/views/import_export.py index d8ef7186f..921455f72 100644 --- a/cookbook/views/import_export.py +++ b/cookbook/views/import_export.py @@ -5,7 +5,7 @@ from json import JSONDecodeError from django.contrib import messages from django.core.files.base import ContentFile -from django.http import HttpResponseRedirect, HttpResponse +from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import render from django.urls import reverse_lazy from django.utils.translation import gettext as _ @@ -23,7 +23,9 @@ def import_recipe(request): form = ImportForm(request.POST) if form.is_valid(): try: - data = json.loads(re.sub(r'"id":([0-9])+,', '', form.cleaned_data['recipe'])) + data = json.loads( + re.sub(r'"id":([0-9])+,', '', form.cleaned_data['recipe']) + ) sr = RecipeSerializer(data=data) if sr.is_valid(): @@ -34,18 +36,39 @@ def import_recipe(request): try: fmt, img = data['image'].split(';base64,') ext = fmt.split('/')[-1] - recipe.image = ContentFile(base64.b64decode(img), name=f'{recipe.pk}.{ext}') # TODO possible security risk, maybe some checks needed + # TODO possible security risk, + # maybe some checks needed + recipe.image = (ContentFile( + base64.b64decode(img), + name=f'{recipe.pk}.{ext}') + ) recipe.save() except ValueError: pass - messages.add_message(request, messages.SUCCESS, _('Recipe imported successfully!')) - return HttpResponseRedirect(reverse_lazy('view_recipe', args=[recipe.pk])) + messages.add_message( + request, + messages.SUCCESS, + _('Recipe imported successfully!') + ) + return HttpResponseRedirect( + reverse_lazy('view_recipe', args=[recipe.pk]) + ) else: - messages.add_message(request, messages.ERROR, _('Something went wrong during the import!')) - messages.add_message(request, messages.WARNING, sr.errors) + messages.add_message( + request, + messages.ERROR, + _('Something went wrong during the import!') + ) + messages.add_message( + request, messages.WARNING, sr.errors + ) except JSONDecodeError: - messages.add_message(request, messages.ERROR, _('Could not parse the supplied JSON!')) + messages.add_message( + request, + messages.ERROR, + _('Could not parse the supplied JSON!') + ) else: form = ImportForm() @@ -65,18 +88,23 @@ def export_recipe(request): if recipe.image and form.cleaned_data['image']: with open(recipe.image.path, 'rb') as img_f: - export['image'] = f'data:image/png;base64,{base64.b64encode(img_f.read()).decode("utf-8")}' + export['image'] = f'data:image/png;base64,{base64.b64encode(img_f.read()).decode("utf-8")}' # noqa: E501 json_string = JSONRenderer().render(export).decode("utf-8") if form.cleaned_data['download']: - response = HttpResponse(json_string, content_type='text/plain') - response['Content-Disposition'] = f'attachment; filename={recipe.name}.json' + response = HttpResponse( + json_string, content_type='text/plain' + ) + response['Content-Disposition'] = f'attachment; filename={recipe.name}.json' # noqa: E501 return response context['export'] = re.sub(r'"id":([0-9])+,', '', json_string) else: - form.add_error('recipe', _('External recipes cannot be exported, please share the file directly or select an internal recipe.')) + form.add_error( + 'recipe', + _('External recipes cannot be exported, please share the file directly or select an internal recipe.') # noqa: E501 + ) else: form = ExportForm() recipe = request.GET.get('r') diff --git a/cookbook/views/lists.py b/cookbook/views/lists.py index 034ddd9d2..6fcdb6124 100644 --- a/cookbook/views/lists.py +++ b/cookbook/views/lists.py @@ -1,6 +1,5 @@ from datetime import datetime -from django.contrib.auth.decorators import login_required from django.db.models import Q from django.db.models.functions import Lower from django.shortcuts import render @@ -9,8 +8,11 @@ from django_tables2 import RequestConfig from cookbook.filters import IngredientFilter, ShoppingListFilter from cookbook.helper.permission_helper import group_required -from cookbook.models import Keyword, SyncLog, RecipeImport, Storage, Food, ShoppingList, InviteLink -from cookbook.tables import KeywordTable, ImportLogTable, RecipeImportTable, StorageTable, IngredientTable, ShoppingListTable, InviteLinkTable +from cookbook.models import (Food, InviteLink, Keyword, RecipeImport, + ShoppingList, Storage, SyncLog) +from cookbook.tables import (ImportLogTable, IngredientTable, InviteLinkTable, + KeywordTable, RecipeImportTable, + ShoppingListTable, StorageTable) @group_required('user') @@ -18,15 +20,27 @@ def keyword(request): table = KeywordTable(Keyword.objects.all()) RequestConfig(request, paginate={'per_page': 25}).configure(table) - return render(request, 'generic/list_template.html', {'title': _("Keyword"), 'table': table, 'create_url': 'new_keyword'}) + return render( + request, + 'generic/list_template.html', + {'title': _("Keyword"), 'table': table, 'create_url': 'new_keyword'} + ) @group_required('admin') def sync_log(request): - table = ImportLogTable(SyncLog.objects.all().order_by(Lower('created_at').desc())) + table = ImportLogTable( + SyncLog.objects.all().order_by( + Lower('created_at').desc() + ) + ) RequestConfig(request, paginate={'per_page': 25}).configure(table) - return render(request, 'generic/list_template.html', {'title': _("Import Log"), 'table': table}) + return render( + request, + 'generic/list_template.html', + {'title': _("Import Log"), 'table': table} + ) @group_required('user') @@ -35,27 +49,52 @@ def recipe_import(request): RequestConfig(request, paginate={'per_page': 25}).configure(table) - return render(request, 'generic/list_template.html', {'title': _("Discovery"), 'table': table, 'import_btn': True}) + return render( + request, + 'generic/list_template.html', + {'title': _("Discovery"), 'table': table, 'import_btn': True} + ) @group_required('user') def food(request): - f = IngredientFilter(request.GET, queryset=Food.objects.all().order_by('pk')) + f = IngredientFilter( + request.GET, + queryset=Food.objects.all().order_by('pk') + ) table = IngredientTable(f.qs) RequestConfig(request, paginate={'per_page': 25}).configure(table) - return render(request, 'generic/list_template.html', {'title': _("Ingredients"), 'table': table, 'filter': f}) + return render( + request, + 'generic/list_template.html', + {'title': _("Ingredients"), 'table': table, 'filter': f} + ) @group_required('user') def shopping_list(request): - f = ShoppingListFilter(request.GET, queryset=ShoppingList.objects.filter(Q(created_by=request.user) | Q(shared=request.user)).all().order_by('finished', 'created_at')) + f = ShoppingListFilter( + request.GET, + queryset=ShoppingList.objects.filter( + Q(created_by=request.user) | + Q(shared=request.user) + ).all().order_by('finished', 'created_at')) table = ShoppingListTable(f.qs) RequestConfig(request, paginate={'per_page': 25}).configure(table) - return render(request, 'generic/list_template.html', {'title': _("Shopping Lists"), 'table': table, 'filter': f, 'create_url': 'view_shopping'}) + return render( + request, + 'generic/list_template.html', + { + 'title': _("Shopping Lists"), + 'table': table, + 'filter': f, + 'create_url': 'view_shopping' + } + ) @group_required('admin') @@ -63,12 +102,31 @@ def storage(request): table = StorageTable(Storage.objects.all()) RequestConfig(request, paginate={'per_page': 25}).configure(table) - return render(request, 'generic/list_template.html', {'title': _("Storage Backend"), 'table': table, 'create_url': 'new_storage'}) + return render( + request, + 'generic/list_template.html', + { + 'title': _("Storage Backend"), + 'table': table, + 'create_url': 'new_storage' + } + ) @group_required('admin') def invite_link(request): - table = InviteLinkTable(InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None).all()) + table = InviteLinkTable( + InviteLink.objects.filter( + valid_until__gte=datetime.today(), used_by=None + ).all()) RequestConfig(request, paginate={'per_page': 25}).configure(table) - return render(request, 'generic/list_template.html', {'title': _("Invite Links"), 'table': table, 'create_url': 'new_invite_link'}) + return render( + request, + 'generic/list_template.html', + { + 'title': _("Invite Links"), + 'table': table, + 'create_url': 'new_invite_link' + } + ) diff --git a/cookbook/views/new.py b/cookbook/views/new.py index 464fbe7cc..877c26021 100644 --- a/cookbook/views/new.py +++ b/cookbook/views/new.py @@ -3,15 +3,17 @@ from datetime import datetime from django.contrib import messages from django.http import HttpResponseRedirect -from django.shortcuts import render, redirect, get_object_or_404 -from django.urls import reverse_lazy, reverse +from django.shortcuts import get_object_or_404, redirect, render +from django.urls import reverse, reverse_lazy from django.utils.translation import gettext as _ from django.views.generic import CreateView -from cookbook.forms import ImportRecipeForm, RecipeImport, KeywordForm, Storage, StorageForm, InternalRecipeForm, \ - RecipeBookForm, MealPlanForm, InviteLinkForm -from cookbook.helper.permission_helper import GroupRequiredMixin, group_required -from cookbook.models import Keyword, Recipe, RecipeBook, MealPlan, ShareLink, MealType, Step, InviteLink +from cookbook.forms import (ImportRecipeForm, InviteLinkForm, KeywordForm, + MealPlanForm, RecipeBookForm, Storage, StorageForm) +from cookbook.helper.permission_helper import (GroupRequiredMixin, + group_required) +from cookbook.models import (InviteLink, Keyword, MealPlan, MealType, Recipe, + RecipeBook, RecipeImport, ShareLink, Step) class RecipeCreate(GroupRequiredMixin, CreateView): @@ -26,7 +28,9 @@ class RecipeCreate(GroupRequiredMixin, CreateView): obj.internal = True obj.save() obj.steps.add(Step.objects.create()) - return HttpResponseRedirect(reverse('edit_recipe', kwargs={'pk': obj.pk})) + return HttpResponseRedirect( + reverse('edit_recipe', kwargs={'pk': obj.pk}) + ) def get_success_url(self): return reverse('edit_recipe', kwargs={'pk': self.object.pk}) @@ -41,7 +45,9 @@ class RecipeCreate(GroupRequiredMixin, CreateView): def share_link(request, pk): recipe = get_object_or_404(Recipe, pk=pk) link = ShareLink.objects.create(recipe=recipe, created_by=request.user) - return HttpResponseRedirect(reverse('view_recipe', kwargs={'pk': pk, 'share': link.uuid})) + return HttpResponseRedirect( + reverse('view_recipe', kwargs={'pk': pk, 'share': link.uuid}) + ) class KeywordCreate(GroupRequiredMixin, CreateView): @@ -68,7 +74,9 @@ class StorageCreate(GroupRequiredMixin, CreateView): obj = form.save(commit=False) obj.created_by = self.request.user obj.save() - return HttpResponseRedirect(reverse('edit_storage', kwargs={'pk': obj.pk})) + return HttpResponseRedirect( + reverse('edit_storage', kwargs={'pk': obj.pk}) + ) def get_context_data(self, **kwargs): context = super(StorageCreate, self).get_context_data(**kwargs) @@ -95,14 +103,25 @@ def create_new_external_recipe(request, import_id): RecipeImport.objects.get(id=import_id).delete() - messages.add_message(request, messages.SUCCESS, _('Imported new recipe!')) + messages.add_message( + request, messages.SUCCESS, _('Imported new recipe!') + ) return redirect('list_recipe_import') else: - messages.add_message(request, messages.ERROR, _('There was an error importing this recipe!')) + messages.add_message( + request, + messages.ERROR, + _('There was an error importing this recipe!') + ) else: new_recipe = RecipeImport.objects.get(id=import_id) form = ImportRecipeForm( - initial={'file_path': new_recipe.file_path, 'name': new_recipe.name, 'file_uid': new_recipe.file_uid}) + initial={ + 'file_path': new_recipe.file_path, + 'name': new_recipe.name, + 'file_uid': new_recipe.file_uid + } + ) return render(request, 'forms/edit_import_recipe.html', {'form': form}) @@ -135,14 +154,28 @@ class MealPlanCreate(GroupRequiredMixin, CreateView): def get_form(self, form_class=None): form = self.form_class(**self.get_form_kwargs()) - form.fields['meal_type'].queryset = MealType.objects.filter(created_by=self.request.user).all() + form.fields['meal_type'].queryset = MealType.objects.filter( + created_by=self.request.user + ).all() return form def get_initial(self): return dict( - meal_type=self.request.GET['meal'] if 'meal' in self.request.GET else None, - date=datetime.strptime(self.request.GET['date'], '%Y-%m-%d') if 'date' in self.request.GET else None, - shared=self.request.user.userpreference.plan_share.all() if self.request.user.userpreference.plan_share else None + meal_type=( + self.request.GET['meal'] + if 'meal' in self.request.GET + else None + ), + date=( + datetime.strptime(self.request.GET['date'], '%Y-%m-%d') + if 'date' in self.request.GET + else None + ), + shared=( + self.request.user.userpreference.plan_share.all() + if self.request.user.userpreference.plan_share + else None + ) ) def form_valid(self, form): @@ -159,7 +192,7 @@ class MealPlanCreate(GroupRequiredMixin, CreateView): if recipe: if re.match(r'^([0-9])+$', recipe): if Recipe.objects.filter(pk=int(recipe)).exists(): - context['default_recipe'] = Recipe.objects.get(pk=int(recipe)) + context['default_recipe'] = Recipe.objects.get(pk=int(recipe)) # noqa: E501 return context diff --git a/cookbook/views/views.py b/cookbook/views/views.py index 10f231307..016d879ab 100644 --- a/cookbook/views/views.py +++ b/cookbook/views/views.py @@ -1,36 +1,40 @@ -import copy import os from datetime import datetime, timedelta +from typing import re from uuid import UUID + +from django.conf import settings from django.contrib import messages -from django.contrib.auth import update_session_auth_hash, authenticate +from django.contrib.auth import update_session_auth_hash from django.contrib.auth.forms import PasswordChangeForm from django.contrib.auth.password_validation import validate_password from django.core.exceptions import ValidationError from django.db import IntegrityError -from django.db.models import Q, Avg +from django.db.models import Avg, Q from django.http import HttpResponseRedirect -from django.shortcuts import render, get_object_or_404 +from django.shortcuts import get_object_or_404, render from django.urls import reverse, reverse_lazy from django.utils import timezone -from django.views.decorators.clickjacking import xframe_options_exempt -from django_tables2 import RequestConfig from django.utils.translation import gettext as _ - -from django.conf import settings +from django_tables2 import RequestConfig from rest_framework.authtoken.models import Token from cookbook.filters import RecipeFilter -from cookbook.forms import * +from cookbook.forms import (CommentForm, Recipe, RecipeBookEntryForm, User, + UserCreateForm, UserNameForm, UserPreference, + UserPreferenceForm) from cookbook.helper.permission_helper import group_required, share_link_valid -from cookbook.tables import RecipeTable, RecipeTableSmall, CookLogTable, ViewLogTable - -from recipes.version import * +from cookbook.models import (Comment, CookLog, InviteLink, MealPlan, + RecipeBook, RecipeBookEntry, ViewLog) +from cookbook.tables import (CookLogTable, RecipeTable, RecipeTableSmall, + ViewLogTable) +from recipes.version import BUILD_REF, VERSION_NUMBER def index(request): if not request.user.is_authenticated: - if User.objects.count() < 1 and 'django.contrib.auth.backends.RemoteUserBackend' not in settings.AUTHENTICATION_BACKENDS: + if (User.objects.count() < 1 + and 'django.contrib.auth.backends.RemoteUserBackend' not in settings.AUTHENTICATION_BACKENDS): # noqa: E501 return HttpResponseRedirect(reverse_lazy('view_setup')) return HttpResponseRedirect(reverse_lazy('view_search')) try: @@ -40,14 +44,19 @@ def index(request): UserPreference.BOOKS: reverse_lazy('view_books'), } - return HttpResponseRedirect(page_map.get(request.user.userpreference.default_page)) + return HttpResponseRedirect( + page_map.get(request.user.userpreference.default_page) + ) except UserPreference.DoesNotExist: return HttpResponseRedirect(reverse('login') + '?next=' + request.path) def search(request): if request.user.is_authenticated: - f = RecipeFilter(request.GET, queryset=Recipe.objects.all().order_by('name')) + f = RecipeFilter( + request.GET, + queryset=Recipe.objects.all().order_by('name') + ) if request.user.userpreference.search_style == UserPreference.LARGE: table = RecipeTable(f.qs) @@ -56,7 +65,10 @@ def search(request): RequestConfig(request, paginate={'per_page': 25}).configure(table) if request.GET == {} and request.user.userpreference.show_recent: - qs = Recipe.objects.filter(viewlog__created_by=request.user).order_by('-viewlog__created_at').all() + qs = Recipe.objects \ + .filter(viewlog__created_by=request.user) \ + .order_by('-viewlog__created_at') \ + .all() recent_list = [] for r in qs: @@ -69,7 +81,11 @@ def search(request): else: last_viewed = None - return render(request, 'index.html', {'recipes': table, 'filter': f, 'last_viewed': last_viewed}) + return render( + request, + 'index.html', + {'recipes': table, 'filter': f, 'last_viewed': last_viewed} + ) else: return HttpResponseRedirect(reverse('login') + '?next=' + request.path) @@ -77,17 +93,30 @@ def search(request): def recipe_view(request, pk, share=None): recipe = get_object_or_404(Recipe, pk=pk) - if not request.user.is_authenticated and not share_link_valid(recipe, share): - messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!')) + if not (request.user.is_authenticated + and not share_link_valid(recipe, share)): + messages.add_message( + request, + messages.ERROR, + _('You do not have the required permissions to view this page!') + ) return HttpResponseRedirect(reverse('login') + '?next=' + request.path) comments = Comment.objects.filter(recipe=recipe) if request.method == "POST": if not request.user.is_authenticated: - messages.add_message(request, messages.ERROR, - _('You do not have the required permissions to perform this action!')) - return HttpResponseRedirect(reverse('view_recipe', kwargs={'pk': recipe.pk, 'share': share})) + messages.add_message( + request, + messages.ERROR, + _('You do not have the required permissions to perform this action!') # noqa: E501 + ) + return HttpResponseRedirect( + reverse( + 'view_recipe', + kwargs={'pk': recipe.pk, 'share': share} + ) + ) comment_form = CommentForm(request.POST, prefix='comment') if comment_form.is_valid(): @@ -98,7 +127,9 @@ def recipe_view(request, pk, share=None): comment.save() - messages.add_message(request, messages.SUCCESS, _('Comment saved!')) + messages.add_message( + request, messages.SUCCESS, _('Comment saved!') + ) bookmark_form = RecipeBookEntryForm(request.POST, prefix='bookmark') if bookmark_form.is_valid(): @@ -110,42 +141,75 @@ def recipe_view(request, pk, share=None): bookmark.save() except IntegrityError as e: if 'UNIQUE constraint' in str(e.args): - messages.add_message(request, messages.ERROR, _('This recipe is already linked to the book!')) + messages.add_message( + request, + messages.ERROR, + _('This recipe is already linked to the book!') + ) else: - messages.add_message(request, messages.SUCCESS, _('Bookmark saved!')) + messages.add_message( + request, + messages.SUCCESS, + _('Bookmark saved!') + ) comment_form = CommentForm() bookmark_form = RecipeBookEntryForm() user_servings = None if request.user.is_authenticated: - user_servings = CookLog.objects.filter(recipe=recipe, created_by=request.user, - servings__gt=0).all().aggregate(Avg('servings'))['servings__avg'] + user_servings = CookLog.objects.filter( + recipe=recipe, + created_by=request.user, + servings__gt=0 + ).all().aggregate(Avg('servings'))['servings__avg'] if request.user.is_authenticated: - if not ViewLog.objects.filter(recipe=recipe).filter(created_by=request.user).filter( - created_at__gt=(timezone.now() - timezone.timedelta(minutes=5))).exists(): + if not ViewLog.objects \ + .filter(recipe=recipe) \ + .filter(created_by=request.user) \ + .filter(created_at__gt=( + timezone.now() - timezone.timedelta(minutes=5))) \ + .exists(): ViewLog.objects.create(recipe=recipe, created_by=request.user) - return render(request, 'recipe_view.html', - {'recipe': recipe, 'comments': comments, 'comment_form': comment_form, - 'bookmark_form': bookmark_form, 'share': share, 'user_servings': user_servings}) + return render( + request, + 'recipe_view.html', + { + 'recipe': recipe, + 'comments': comments, + 'comment_form': comment_form, + 'bookmark_form': bookmark_form, + 'share': share, + 'user_servings': user_servings + } + ) @group_required('user') def books(request): book_list = [] - books = RecipeBook.objects.filter(Q(created_by=request.user) | Q(shared=request.user)).distinct().all() + books = RecipeBook.objects.filter( + Q(created_by=request.user) | Q(shared=request.user) + ).distinct().all() for b in books: - book_list.append({'book': b, 'recipes': RecipeBookEntry.objects.filter(book=b).all()}) + book_list.append( + { + 'book': b, + 'recipes': RecipeBookEntry.objects.filter(book=b).all() + } + ) return render(request, 'books.html', {'book_list': book_list}) def get_start_end_from_week(p_year, p_week): - first_day_of_week = datetime.strptime(f'{p_year}-W{int(p_week) - 1}-1', "%Y-W%W-%w").date() + first_day_of_week = datetime.strptime( + f'{p_year}-W{int(p_week) - 1}-1', "%Y-W%W-%w" + ).date() last_day_of_week = first_day_of_week + timedelta(days=6.9) return first_day_of_week, last_day_of_week @@ -168,13 +232,24 @@ def meal_plan_entry(request, pk): plan = MealPlan.objects.get(pk=pk) if plan.created_by != request.user and plan.shared != request.user: - messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!')) + messages.add_message( + request, + messages.ERROR, + _('You do not have the required permissions to view this page!') + ) return HttpResponseRedirect(reverse_lazy('index')) - same_day_plan = MealPlan.objects.filter(date=plan.date).exclude(pk=plan.pk).filter( - Q(created_by=request.user) | Q(shared=request.user)).order_by('meal_type').all() + same_day_plan = MealPlan.objects \ + .filter(date=plan.date) \ + .exclude(pk=plan.pk) \ + .filter(Q(created_by=request.user) | Q(shared=request.user)) \ + .order_by('meal_type').all() - return render(request, 'meal_plan_entry.html', {'plan': plan, 'same_day_plan': same_day_plan}) + return render( + request, + 'meal_plan_entry.html', + {'plan': plan, 'same_day_plan': same_day_plan} + ) @group_required('user') @@ -189,7 +264,11 @@ def shopping_list(request, pk=None): if recipe := Recipe.objects.filter(pk=int(rid)).first(): recipes.append({'recipe': recipe.id, 'multiplier': multiplier}) - return render(request, 'shopping_list.html', {'shopping_list_id': pk, 'recipes': recipes}) + return render( + request, + 'shopping_list.html', + {'shopping_list_id': pk, 'recipes': recipes} + ) @group_required('guest') @@ -214,22 +293,22 @@ def user_settings(request): up.show_recent = form.cleaned_data['show_recent'] up.search_style = form.cleaned_data['search_style'] up.plan_share.set(form.cleaned_data['plan_share']) - up.ingredient_decimals = form.cleaned_data['ingredient_decimals'] + up.ingredient_decimals = form.cleaned_data['ingredient_decimals'] # noqa: E501 up.comments = form.cleaned_data['comments'] up.use_fractions = form.cleaned_data['use_fractions'] up.sticky_navbar = form.cleaned_data['sticky_navbar'] up.shopping_auto_sync = form.cleaned_data['shopping_auto_sync'] - if up.shopping_auto_sync < settings.SHOPPING_MIN_AUTOSYNC_INTERVAL: - up.shopping_auto_sync = settings.SHOPPING_MIN_AUTOSYNC_INTERVAL + if up.shopping_auto_sync < settings.SHOPPING_MIN_AUTOSYNC_INTERVAL: # noqa: E501 + up.shopping_auto_sync = settings.SHOPPING_MIN_AUTOSYNC_INTERVAL # noqa: E501 up.save() if 'user_name_form' in request.POST: user_name_form = UserNameForm(request.POST, prefix='name') if user_name_form.is_valid(): - request.user.first_name = user_name_form.cleaned_data['first_name'] - request.user.last_name = user_name_form.cleaned_data['last_name'] + request.user.first_name = user_name_form.cleaned_data['first_name'] # noqa: E501 + request.user.last_name = user_name_form.cleaned_data['last_name'] # noqa: E501 request.user.save() if 'password_form' in request.POST: @@ -246,40 +325,74 @@ def user_settings(request): if (api_token := Token.objects.filter(user=request.user).first()) is None: api_token = Token.objects.create(user=request.user) - return render(request, 'settings.html', - {'preference_form': preference_form, 'user_name_form': user_name_form, 'password_form': password_form, - 'api_token': api_token}) + return render( + request, + 'settings.html', + { + 'preference_form': preference_form, + 'user_name_form': user_name_form, + 'password_form': password_form, + 'api_token': api_token + } + ) @group_required('guest') def history(request): - view_log = ViewLogTable(ViewLog.objects.filter(created_by=request.user).order_by('-created_at').all()) - cook_log = CookLogTable(CookLog.objects.filter(created_by=request.user).order_by('-created_at').all()) - return render(request, 'history.html', {'view_log': view_log, 'cook_log': cook_log}) + view_log = ViewLogTable( + ViewLog.objects.filter( + created_by=request.user + ).order_by('-created_at').all() + ) + cook_log = CookLogTable( + CookLog.objects.filter( + created_by=request.user + ).order_by('-created_at').all() + ) + return render( + request, + 'history.html', + {'view_log': view_log, 'cook_log': cook_log} + ) @group_required('admin') def system(request): - postgres = False if (settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql_psycopg2' or - settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql') else True + postgres = False if ( + settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql_psycopg2' # noqa: E501 + or settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql' # noqa: E501 + ) else True secret_key = False if os.getenv('SECRET_KEY') else True - return render(request, 'system.html', - {'gunicorn_media': settings.GUNICORN_MEDIA, 'debug': settings.DEBUG, 'postgres': postgres, - 'version': VERSION_NUMBER, 'ref': BUILD_REF, 'secret_key': secret_key}) + return render( + request, + 'system.html', + { + 'gunicorn_media': settings.GUNICORN_MEDIA, + 'debug': settings.DEBUG, + 'postgres': postgres, + 'version': VERSION_NUMBER, + 'ref': BUILD_REF, + 'secret_key': secret_key + } + ) def setup(request): - if User.objects.count() > 0 or 'django.contrib.auth.backends.RemoteUserBackend' in settings.AUTHENTICATION_BACKENDS: - messages.add_message(request, messages.ERROR, _( - 'The setup page can only be used to create the first user! If you have forgotten your superuser credentials please consult the django documentation on how to reset passwords.')) + if (User.objects.count() > 0 + or 'django.contrib.auth.backends.RemoteUserBackend' in settings.AUTHENTICATION_BACKENDS): # noqa: E501 + messages.add_message( + request, + messages.ERROR, + _('The setup page can only be used to create the first user! If you have forgotten your superuser credentials please consult the django documentation on how to reset passwords.') # noqa: E501 + ) return HttpResponseRedirect(reverse('login')) if request.method == 'POST': form = UserCreateForm(request.POST) if form.is_valid(): - if form.cleaned_data['password'] != form.cleaned_data['password_confirm']: + if form.cleaned_data['password'] != form.cleaned_data['password_confirm']: # noqa: E501 form.add_error('password', _('Passwords dont match!')) else: user = User( @@ -291,7 +404,11 @@ def setup(request): validate_password(form.cleaned_data['password'], user=user) user.set_password(form.cleaned_data['password']) user.save() - messages.add_message(request, messages.SUCCESS, _('User has been created, please login!')) + messages.add_message( + request, + messages.SUCCESS, + _('User has been created, please login!') + ) return HttpResponseRedirect(reverse('login')) except ValidationError as e: for m in e: @@ -306,10 +423,14 @@ def signup(request, token): try: token = UUID(token, version=4) except ValueError: - messages.add_message(request, messages.ERROR, _('Malformed Invite Link supplied!')) + messages.add_message( + request, messages.ERROR, _('Malformed Invite Link supplied!') + ) return HttpResponseRedirect(reverse('index')) - if link := InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None, uuid=token).first(): + if link := InviteLink.objects.filter( + valid_until__gte=datetime.today(), used_by=None, uuid=token) \ + .first(): if request.method == 'POST': form = UserCreateForm(request.POST) @@ -319,17 +440,23 @@ def signup(request, token): form.data = data if form.is_valid(): - if form.cleaned_data['password'] != form.cleaned_data['password_confirm']: + if form.cleaned_data['password'] != form.cleaned_data['password_confirm']: # noqa: E501 form.add_error('password', _('Passwords dont match!')) else: user = User( username=form.cleaned_data['name'], ) try: - validate_password(form.cleaned_data['password'], user=user) + validate_password( + form.cleaned_data['password'], user=user + ) user.set_password(form.cleaned_data['password']) user.save() - messages.add_message(request, messages.SUCCESS, _('User has been created, please login!')) + messages.add_message( + request, + messages.SUCCESS, + _('User has been created, please login!') + ) link.used_by = user link.save() @@ -344,9 +471,13 @@ def signup(request, token): if link.username != '': form.fields['name'].initial = link.username form.fields['name'].disabled = True - return render(request, 'registration/signup.html', {'form': form, 'link': link}) + return render( + request, 'registration/signup.html', {'form': form, 'link': link} + ) - messages.add_message(request, messages.ERROR, _('Invite Link not valid or already used!')) + messages.add_message( + request, messages.ERROR, _('Invite Link not valid or already used!') + ) return HttpResponseRedirect(reverse('index')) diff --git a/recipes/middleware.py b/recipes/middleware.py index 662d61d8a..b3efd3664 100644 --- a/recipes/middleware.py +++ b/recipes/middleware.py @@ -1,5 +1,7 @@ -from django.contrib.auth.middleware import RemoteUserMiddleware from os import getenv +from django.contrib.auth.middleware import RemoteUserMiddleware + + class CustomRemoteUser(RemoteUserMiddleware): header = getenv('PROXY_HEADER', 'HTTP_REMOTE_USER') diff --git a/recipes/settings.py b/recipes/settings.py index 5143f1110..628d093e3 100644 --- a/recipes/settings.py +++ b/recipes/settings.py @@ -14,8 +14,8 @@ import random import string from django.contrib import messages -from dotenv import load_dotenv from django.utils.translation import gettext_lazy as _ +from dotenv import load_dotenv BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) diff --git a/recipes/urls.py b/recipes/urls.py index 09d4e2fa4..d975d22ca 100644 --- a/recipes/urls.py +++ b/recipes/urls.py @@ -1,4 +1,5 @@ -"""recipes URL Configuration +""" +recipes URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/2.0/topics/http/urls/ @@ -15,9 +16,8 @@ Including another URLconf """ from django.conf import settings from django.conf.urls import url -from django.conf.urls.static import static -from django.urls import include, path from django.contrib import admin +from django.urls import include, path from django.views.i18n import JavaScriptCatalog from django.views.static import serve @@ -26,8 +26,16 @@ urlpatterns = [ path('admin/', admin.site.urls), path('accounts/', include('django.contrib.auth.urls')), path('i18n/', include('django.conf.urls.i18n')), - path('jsi18n/', JavaScriptCatalog.as_view(domain='django'), name='javascript-catalog'), + path( + 'jsi18n/', + JavaScriptCatalog.as_view(domain='django'), + name='javascript-catalog' + ), ] if settings.GUNICORN_MEDIA or settings.DEBUG: - urlpatterns += url(r'^media/(?P.*)$', serve, {'document_root': settings.MEDIA_ROOT}), + urlpatterns += url( + r'^media/(?P.*)$', + serve, + {'document_root': settings.MEDIA_ROOT} + ),