Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
vabene1111 committed Jul 7, 2022
2 parents 0e2a27a + fd4051c commit 5b3d8a3
Show file tree
Hide file tree
Showing 162 changed files with 21,428 additions and 67,925 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ jobs:
run: yarn build
- name: Install Django dependencies
run: |
sudo apt-get -y update
sudo apt-get install -y libsasl2-dev python-dev libldap2-dev libssl-dev
python -m pip install --upgrade pip
pip install -r requirements.txt
Expand Down
1 change: 1 addition & 0 deletions .idea/dictionaries/vabene1111_PC.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ WORKDIR /opt/recipes

COPY requirements.txt ./

RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-dev jpeg-dev libwebp-dev openssl-dev libffi-dev cargo openldap-dev python3-dev && \
RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-dev jpeg-dev libwebp-dev openssl-dev libffi-dev cargo openldap-dev python3-dev git && \
echo -n "INPUT ( libldap.so )" > /usr/lib/libldap_r.so && \
python -m venv venv && \
/opt/recipes/venv/bin/python -m pip install --upgrade pip && \
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile-raspi
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ RUN \
if [ `apk --print-arch` = "armv7" ]; then \
printf "[global]\nextra-index-url=https://www.piwheels.org/simple\n" > /etc/pip.conf ; \
fi
RUN apk add --no-cache --virtual .build-deps gcc musl-dev zlib-dev jpeg-dev libwebp-dev python3-dev && \
RUN apk add --no-cache --virtual .build-deps gcc musl-dev zlib-dev jpeg-dev libwebp-dev python3-dev git && \
echo -n "INPUT ( libldap.so )" > /usr/lib/libldap_r.so && \
python -m venv venv && \
/opt/recipes/venv/bin/python -m pip install --upgrade pip && \
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,15 @@ a public page.

Documentation can be found [here](https://docs.tandoor.dev/).

## Support our work
Tandoor is developed by volunteers in their free time just because its fun. That said earning
some money with the project allows us to spend more time on it and thus make improvements we otherwise couldn't.
Because of that there are several ways you can support us

- **GitHub Sponsors** You can sponsor contributors of this project on GitHub: [vabene1111](https://github.com/sponsors/vabene1111)
- **Host at Hetzner** We have been very happy customers of Hetzner for multiple years for all of our projects. If you want to get into self-hosting or are tired of the expensive big providers, their cloud servers are a great place to get started. When you sign up via our [referral link](https://hetzner.cloud/?ref=ISdlrLmr9kGj) you will get 20€ worth of cloud credits and we get a small kickback too.
- **Let us host for you** We are offering a [hosted version](https://app.tandoor.dev) where all profits support us and the development of tandoor (currently only available in germany).

## Contributing

You can help out with the ongoing development by looking for potential bugs in our code base, or by contributing new features. We are always welcoming new pull requests containing bug fixes, refactors and new features. We have a list of tasks and bugs on our issue tracker on Github. Please comment on issues if you want to contribute with, to avoid duplicating effort.
Expand Down
40 changes: 3 additions & 37 deletions cookbook/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,41 +32,7 @@ def has_add_permission(self, request, obj=None):
@admin.action(description='Delete all data from a space')
def delete_space_action(modeladmin, request, queryset):
for space in queryset:
CookLog.objects.filter(space=space).delete()
ViewLog.objects.filter(space=space).delete()
ImportLog.objects.filter(space=space).delete()
BookmarkletImport.objects.filter(space=space).delete()

Comment.objects.filter(recipe__space=space).delete()
Keyword.objects.filter(space=space).delete()
Ingredient.objects.filter(space=space).delete()
Food.objects.filter(space=space).delete()
Unit.objects.filter(space=space).delete()
Step.objects.filter(space=space).delete()
NutritionInformation.objects.filter(space=space).delete()
RecipeBookEntry.objects.filter(book__space=space).delete()
RecipeBook.objects.filter(space=space).delete()
MealType.objects.filter(space=space).delete()
MealPlan.objects.filter(space=space).delete()
ShareLink.objects.filter(space=space).delete()
Recipe.objects.filter(space=space).delete()

RecipeImport.objects.filter(space=space).delete()
SyncLog.objects.filter(sync__space=space).delete()
Sync.objects.filter(space=space).delete()
Storage.objects.filter(space=space).delete()

ShoppingListEntry.objects.filter(shoppinglist__space=space).delete()
ShoppingListRecipe.objects.filter(shoppinglist__space=space).delete()
ShoppingList.objects.filter(space=space).delete()

SupermarketCategoryRelation.objects.filter(supermarket__space=space).delete()
SupermarketCategory.objects.filter(space=space).delete()
Supermarket.objects.filter(space=space).delete()

InviteLink.objects.filter(space=space).delete()
UserFile.objects.filter(space=space).delete()
Automation.objects.filter(space=space).delete()
space.save()


class SpaceAdmin(admin.ModelAdmin):
Expand All @@ -81,8 +47,8 @@ class SpaceAdmin(admin.ModelAdmin):


class UserPreferenceAdmin(admin.ModelAdmin):
list_display = ('name', 'space', 'theme', 'nav_color', 'default_page', 'search_style',) # TODO add new fields
search_fields = ('user__username', 'space__name')
list_display = ('name', 'theme', 'nav_color', 'default_page', 'search_style',) # TODO add new fields
search_fields = ('user__username',)
list_filter = ('theme', 'nav_color', 'default_page', 'search_style')
date_hierarchy = 'created_at'

Expand Down
7 changes: 2 additions & 5 deletions cookbook/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,9 @@ class UserPreferenceForm(forms.ModelForm):
prefix = 'preference'

def __init__(self, *args, **kwargs):
if x := kwargs.get('instance', None):
space = x.space
else:
space = kwargs.pop('space')
space = kwargs.pop('space')
super().__init__(*args, **kwargs)
self.fields['plan_share'].queryset = User.objects.filter(userpreference__space=space).all()
self.fields['plan_share'].queryset = User.objects.filter(userspace__space=space).all()

class Meta:
model = UserPreference
Expand Down
76 changes: 67 additions & 9 deletions cookbook/helper/permission_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
from django.contrib import messages
from django.contrib.auth.decorators import user_passes_test
from django.core.cache import caches
from django.core.exceptions import ValidationError
from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.http import HttpResponseRedirect
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, Recipe, UserPreference
from cookbook.models import ShareLink, Recipe, UserPreference, UserSpace


def get_allowed_groups(groups_required):
Expand Down Expand Up @@ -40,8 +40,11 @@ def has_group_permission(user, groups):
return False
groups_allowed = get_allowed_groups(groups)
if user.is_authenticated:
if bool(user.groups.filter(name__in=groups_allowed)):
return True
if user_space := user.userspace_set.filter(active=True):
if len(user_space) != 1:
return False # do not allow any group permission if more than one space is active, needs to be changed when simultaneous multi-space-tenancy is added
if bool(user_space.first().groups.filter(name__in=groups_allowed)):
return True
return False


Expand All @@ -50,7 +53,6 @@ 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)
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
Expand All @@ -63,11 +65,25 @@ def is_object_owner(user, obj):
return False


def is_space_owner(user, obj):
"""
Tests if a given user is the owner the space of a given object
:param user django auth user object
:param obj any object that should be tested
:return: true if user is owner of the objects space, false otherwise
"""
if not user.is_authenticated:
return False
try:
return obj.get_space().get_owner() == user
except Exception:
return False


def is_object_shared(user, obj):
"""
Tests if a given user is shared for a given object
test performed by checking user against the objects shared table
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 shared for object, false otherwise
Expand Down Expand Up @@ -163,7 +179,7 @@ def dispatch(self, request, *args, **kwargs):

try:
obj = self.get_object()
if obj.get_space() != request.space:
if not request.user.userspace.filter(space=obj.get_space()).exists():
messages.add_message(request, messages.ERROR,
_('You do not have the required permissions to view this page!'))
return HttpResponseRedirect(reverse_lazy('index'))
Expand All @@ -181,7 +197,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!') # noqa: E501
message = _('You cannot interact with this object as it is not owned by you!')

def has_permission(self, request, view):
return request.user.is_authenticated
Expand All @@ -190,6 +206,28 @@ def has_object_permission(self, request, view, obj):
return is_object_owner(request.user, obj)


class CustomIsOwnerReadOnly(CustomIsOwner):
def has_permission(self, request, view):
return super().has_permission(request, view) and request.method in SAFE_METHODS

def has_object_permission(self, request, view, obj):
return super().has_object_permission(request, view) and request.method in SAFE_METHODS


class CustomIsSpaceOwner(permissions.BasePermission):
"""
Custom permission class for django rest framework views
verifies if the user is the owner of the space the object belongs to
"""
message = _('You cannot interact with this object as it is not owned by you!')

def has_permission(self, request, view):
return request.user.is_authenticated and request.space.created_by == request.user

def has_object_permission(self, request, view, obj):
return is_space_owner(request.user, obj)


# TODO function duplicate/too similar name
class CustomIsShared(permissions.BasePermission):
"""
Expand Down Expand Up @@ -290,7 +328,27 @@ def above_space_user_limit(space):
:param space: Space to test for limits
:return: Tuple (True if above or equal limit else false, message)
"""
limit = space.max_users != 0 and UserPreference.objects.filter(space=space).count() > space.max_users
limit = space.max_users != 0 and UserSpace.objects.filter(space=space).count() > space.max_users
if limit:
return True, _('You have more users than allowed in your space.')
return False, ''


def switch_user_active_space(user, space):
"""
Switch the currently active space of a user by setting all spaces to inactive and activating the one passed
:param user: user to change active space for
:param space: space to activate user for
:return user space object or none if not found/no permission
"""
try:
us = UserSpace.objects.get(space=space, user=user)
if not us.active:
UserSpace.objects.filter(user=user).update(active=False)
us.active = True
us.save()
return us
else:
return us
except ObjectDoesNotExist:
return None
12 changes: 5 additions & 7 deletions cookbook/helper/recipe_html_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@

from bs4 import BeautifulSoup
from bs4.element import Tag
from recipe_scrapers import scrape_html, scrape_me
from recipe_scrapers._exceptions import NoSchemaFoundInWildMode
from recipe_scrapers._utils import get_host_name, normalize_string

from cookbook.helper import recipe_url_import as helper
from cookbook.helper.scrapers.scrapers import text_scraper
from recipe_scrapers import scrape_me
from recipe_scrapers._exceptions import NoSchemaFoundInWildMode


def get_recipe_from_source(text, url, request):
Expand Down Expand Up @@ -62,8 +62,9 @@ def get_children_list(children):

recipe_tree = []
parse_list = []
html_data = []
images = []
soup = BeautifulSoup(text, "html.parser")
html_data = get_from_html(soup)
images = get_images_from_source(soup, url)
text = unquote(text)
scrape = None

Expand All @@ -80,9 +81,6 @@ def get_children_list(children):
scrape = text_scraper("<script type='application/ld+json'>" + text + "</script>", url=url)

except JSONDecodeError:
soup = BeautifulSoup(text, "html.parser")
html_data = get_from_html(soup)
images += get_images_from_source(soup, url)
for el in soup.find_all('script', type='application/ld+json'):
el = remove_graph(el)
if not url and 'url' in el:
Expand Down
23 changes: 14 additions & 9 deletions cookbook/helper/recipe_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector, TrigramSimilarity
from django.core.cache import caches
from django.db.models import Avg, Case, Count, F, Func, Max, OuterRef, Q, Subquery, Sum, Value, When
from django.db.models import (Avg, Case, Count, Exists, F, Func, Max, OuterRef, Q, Subquery, Sum,
Value, When)
from django.db.models.functions import Coalesce, Lower, Substr
from django.utils import timezone, translation
from django.utils.translation import gettext as _
Expand Down Expand Up @@ -525,30 +526,34 @@ def _makenow_filter(self, missing=None):
@ staticmethod
def __children_substitute_filter(shopping_users=None):
children_onhand_subquery = Food.objects.filter(
path__startswith=Substr(OuterRef('path'), 1, Food.steplen*OuterRef('depth')),
path__startswith=OuterRef('path'),
depth__gt=OuterRef('depth'),
onhand_users__in=shopping_users
).annotate(child_onhand=Coalesce(Func('pk', function='Count'), 0)).values('child_onhand')
)
return Food.objects.exclude( # list of foods that are onhand and children of: foods that are not onhand and are set to use children as substitutes
Q(onhand_users__in=shopping_users)
| Q(ignore_shopping=True, recipe__isnull=True)
| Q(substitute__onhand_users__in=shopping_users)
).exclude(depth=1, numchild=0).filter(substitute_children=True
).annotate(child_onhand=Coalesce(Subquery(children_onhand_subquery), 0)).exclude(child_onhand=0)
).exclude(depth=1, numchild=0
).filter(substitute_children=True
).annotate(child_onhand_count=Exists(children_onhand_subquery)
).filter(child_onhand_count=True)

@ staticmethod
def __sibling_substitute_filter(shopping_users=None):
sibling_onhand_subquery = Food.objects.filter(
path__startswith=Substr(OuterRef('path'), 1, Food.steplen*(OuterRef('depth')-1)),
depth=OuterRef('depth'),
onhand_users__in=shopping_users
).annotate(sibling_onhand=Coalesce(Func('pk', function='Count'), 0)).values('sibling_onhand')
)
return Food.objects.exclude( # list of foods that are onhand and siblings of: foods that are not onhand and are set to use siblings as substitutes
Q(onhand_users__in=shopping_users)
| Q(ignore_shopping=True, recipe__isnull=True)
| Q(substitute__onhand_users__in=shopping_users)
).exclude(depth=1, numchild=0).filter(substitute_siblings=True
).annotate(sibling_onhand=Coalesce(Subquery(sibling_onhand_subquery), 0)).exclude(sibling_onhand=0)
).exclude(depth=1, numchild=0
).filter(substitute_siblings=True
).annotate(sibling_onhand=Exists(sibling_onhand_subquery)
).filter(sibling_onhand=True)


class RecipeFacet():
Expand Down Expand Up @@ -760,6 +765,6 @@ def old_search(request):
params = dict(request.GET)
params['internal'] = None
f = RecipeFilter(params,
queryset=Recipe.objects.filter(space=request.user.userpreference.space).all().order_by(Lower('name').asc()),
queryset=Recipe.objects.filter(space=request.space).all().order_by(Lower('name').asc()),
space=request.space)
return f.qs
Loading

0 comments on commit 5b3d8a3

Please sign in to comment.