Merge branch 'develop' into importer-reciepekeeper

This commit is contained in:
vabene1111
2021-06-09 16:42:28 +02:00
committed by GitHub
161 changed files with 72695 additions and 2098 deletions

View File

@@ -31,13 +31,15 @@ from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest,
CustomIsOwner, CustomIsShare,
CustomIsShared, CustomIsUser,
group_required)
from cookbook.helper.recipe_html_import import get_recipe_from_source
from cookbook.helper.recipe_search import search_recipes
from cookbook.helper.recipe_url_import import get_from_html, get_from_scraper, find_recipe_json
from cookbook.helper.recipe_url_import import get_from_scraper
from cookbook.models import (CookLog, Food, Ingredient, Keyword, MealPlan,
MealType, Recipe, RecipeBook, ShoppingList,
ShoppingListEntry, ShoppingListRecipe, Step,
Storage, Sync, SyncLog, Unit, UserPreference,
ViewLog, RecipeBookEntry, Supermarket, ImportLog)
ViewLog, RecipeBookEntry, Supermarket, ImportLog, BookmarkletImport, SupermarketCategory, UserFile)
from cookbook.provider.dropbox import Dropbox
from cookbook.provider.local import Local
from cookbook.provider.nextcloud import Nextcloud
@@ -53,8 +55,8 @@ from cookbook.serializer import (FoodSerializer, IngredientSerializer,
SyncSerializer, UnitSerializer,
UserNameSerializer, UserPreferenceSerializer,
ViewLogSerializer, CookLogSerializer, RecipeBookEntrySerializer,
RecipeOverviewSerializer, SupermarketSerializer, ImportLogSerializer)
from recipes.settings import DEMO
RecipeOverviewSerializer, SupermarketSerializer, ImportLogSerializer,
BookmarkletImportSerializer, SupermarketCategorySerializer, UserFileSerializer)
class StandardFilterMixin(ViewSetMixin):
@@ -155,6 +157,16 @@ class SupermarketViewSet(viewsets.ModelViewSet, StandardFilterMixin):
return super().get_queryset()
class SupermarketCategoryViewSet(viewsets.ModelViewSet, StandardFilterMixin):
queryset = SupermarketCategory.objects
serializer_class = SupermarketCategorySerializer
permission_classes = [CustomIsUser]
def get_queryset(self):
self.queryset = self.queryset.filter(space=self.request.space)
return super().get_queryset()
class KeywordViewSet(viewsets.ModelViewSet, StandardFilterMixin):
"""
list:
@@ -227,8 +239,8 @@ class MealPlanViewSet(viewsets.ModelViewSet):
def get_queryset(self):
queryset = self.queryset.filter(
Q(created_by=self.request.user) |
Q(shared=self.request.user)
Q(created_by=self.request.user)
| Q(shared=self.request.user)
).filter(space=self.request.space).distinct().all()
from_date = self.request.query_params.get('from_date', None)
@@ -285,7 +297,7 @@ class RecipeSchema(AutoSchema):
def get_path_parameters(self, path, method):
if not is_list_view(path, method, self.view):
return []
return super(RecipeSchema, self).get_path_parameters(path, method)
parameters = super().get_path_parameters(path, method)
parameters.append({
@@ -351,7 +363,7 @@ class RecipeViewSet(viewsets.ModelViewSet):
if not (share and self.detail):
self.queryset = self.queryset.filter(space=self.request.space)
self.queryset = search_recipes(self.queryset, self.request.GET)
self.queryset = search_recipes(self.request, self.queryset, self.request.GET)
return super().get_queryset()
@@ -377,7 +389,7 @@ class RecipeViewSet(viewsets.ModelViewSet):
obj, data=request.data, partial=True
)
if DEMO:
if self.request.space.demo:
raise PermissionDenied(detail='Not available in demo', code=None)
if serializer.is_valid():
@@ -474,6 +486,27 @@ class ImportLogViewSet(viewsets.ModelViewSet):
return self.queryset.filter(space=self.request.space).all()
class BookmarkletImportViewSet(viewsets.ModelViewSet):
queryset = BookmarkletImport.objects
serializer_class = BookmarkletImportSerializer
permission_classes = [CustomIsUser]
def get_queryset(self):
return self.queryset.filter(space=self.request.space).all()
class UserFileViewSet(viewsets.ModelViewSet, StandardFilterMixin):
queryset = UserFile.objects
serializer_class = UserFileSerializer
permission_classes = [CustomIsUser]
parser_classes = [MultiPartParser]
def get_queryset(self):
self.queryset = self.queryset.filter(space=self.request.space).all()
return super().get_queryset()
# -------------- non django rest api views --------------------
def get_recipe_provider(recipe):
@@ -515,7 +548,7 @@ def get_recipe_file(request, recipe_id):
@group_required('user')
def sync_all(request):
if DEMO:
if request.space.demo:
messages.add_message(
request, messages.ERROR, _('This feature is not available in the demo version!')
)
@@ -599,85 +632,91 @@ def get_plan_ical(request, from_date, to_date):
@group_required('user')
def recipe_from_url(request):
url = request.POST['url']
def recipe_from_source(request):
url = request.POST.get('url', None)
data = request.POST.get('data', None)
mode = request.POST.get('mode', None)
auto = request.POST.get('auto', 'true')
try:
scrape = scrape_me(url)
except WebsiteNotImplementedError:
HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.7) Gecko/2009021910 Firefox/3.0.7"
}
if (not url and not data) or (mode == 'url' and not url) or (mode == 'source' and not data):
return JsonResponse(
{
'error': True,
'msg': _('Nothing to do.')
},
status=400
)
if mode == 'url' and auto == 'true':
try:
scrape = scrape_me(url, wild_mode=True)
except NoSchemaFoundInWildMode:
scrape = scrape_me(url)
except (WebsiteNotImplementedError, AttributeError):
try:
scrape = scrape_me(url, wild_mode=True)
except NoSchemaFoundInWildMode:
return JsonResponse(
{
'error': True,
'msg': _('The requested site provided malformed data and cannot be read.') # noqa: E501
},
status=400)
except ConnectionError:
return JsonResponse(
{
'error': True,
'msg': _('The requested site provided malformed data and cannot be read.') # noqa: E501
'msg': _('The requested page could not be found.')
},
status=400
)
if len(scrape.ingredients()) and len(scrape.instructions()) == 0:
return JsonResponse(
{
'error': True,
'msg': _(
'The requested site does not provide any recognized data format to import the recipe from.')
# noqa: E501
},
status=400)
except ConnectionError:
else:
return JsonResponse({"recipe_json": get_from_scraper(scrape, request.space)})
elif (mode == 'source') or (mode == 'url' and auto == 'false'):
if not data or data == 'undefined':
data = requests.get(url, headers=HEADERS).content
recipe_json, recipe_tree, recipe_html, images = get_recipe_from_source(data, url, request.space)
if len(recipe_tree) == 0 and len(recipe_json) == 0:
return JsonResponse(
{
'error': True,
'msg': _('No useable data could be found.')
},
status=400
)
else:
return JsonResponse({
'recipe_tree': recipe_tree,
'recipe_json': recipe_json,
'recipe_html': recipe_html,
'images': images,
})
else:
return JsonResponse(
{
'error': True,
'msg': _('The requested page could not be found.')
},
status=400
)
return JsonResponse(get_from_scraper(scrape, request.space))
@group_required('user')
def recipe_from_url_old(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'
# 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.')
'msg': _('I couldn\'t find anything to do.')
},
status=400
)
if response.status_code == 403:
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, request.space)
@group_required('user')
def recipe_from_json(request):
mjson = request.POST['json']
md_json = json.loads(mjson)
for ld_json_item in md_json:
# recipes type might be wrapped in @graph type
if '@graph' in ld_json_item:
for x in md_json['@graph']:
if '@type' in x and x['@type'] == 'Recipe':
md_json = x
if ('@type' in md_json
and md_json['@type'] == 'Recipe'):
return JsonResponse(find_recipe_json(md_json, '', request.space))
return JsonResponse(
{
'error': True,
'msg': _('Could not parse correctly...')
},
status=400
)
@group_required('admin')
def get_backup(request):
if not request.user.is_superuser:
return HttpResponse('', status=403)
@group_required('user')
@@ -690,6 +729,7 @@ def ingredient_from_string(request):
'amount': amount,
'unit': unit,
'food': food,
'note': note
},
status=200
)

View File

@@ -17,15 +17,23 @@ from PIL import Image, UnidentifiedImageError
from requests.exceptions import MissingSchema
from cookbook.forms import BatchEditForm, SyncForm
from cookbook.helper.permission_helper import (group_required,
has_group_permission)
from cookbook.helper.permission_helper import group_required, has_group_permission
from cookbook.helper.recipe_url_import import parse_cooktime
from cookbook.models import (Comment, Food, Ingredient, Keyword, Recipe,
RecipeImport, Step, Sync, Unit)
RecipeImport, Step, Sync, Unit, UserPreference)
from cookbook.tables import SyncTable
@group_required('user')
def sync(request):
if request.space.max_recipes != 0 and Recipe.objects.filter(space=request.space).count() >= request.space.max_recipes: # TODO move to central helper function
messages.add_message(request, messages.WARNING, _('You have reached the maximum number of recipes for your space.'))
return HttpResponseRedirect(reverse('index'))
if request.space.max_users != 0 and UserPreference.objects.filter(space=request.space).count() > request.space.max_users:
messages.add_message(request, messages.WARNING, _('You have more users than allowed in your space.'))
return HttpResponseRedirect(reverse('index'))
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!'))
@@ -109,8 +117,18 @@ def batch_edit(request):
@group_required('user')
@atomic
def import_url(request):
if request.space.max_recipes != 0 and Recipe.objects.filter(space=request.space).count() >= request.space.max_recipes: # TODO move to central helper function
messages.add_message(request, messages.WARNING, _('You have reached the maximum number of recipes for your space.'))
return HttpResponseRedirect(reverse('index'))
if request.space.max_users != 0 and UserPreference.objects.filter(space=request.space).count() > request.space.max_users:
messages.add_message(request, messages.WARNING, _('You have more users than allowed in your space.'))
return HttpResponseRedirect(reverse('index'))
if request.method == 'POST':
data = json.loads(request.body)
data['cookTime'] = parse_cooktime(data.get('cookTime', ''))
data['prepTime'] = parse_cooktime(data.get('prepTime', ''))
recipe = Recipe.objects.create(
name=data['name'],
@@ -130,7 +148,7 @@ 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'], space=request.space).first()):
if k := Keyword.objects.filter(name=kw['text'], space=request.space).first():
recipe.keywords.add(k)
elif data['all_keywords']:
k = Keyword.objects.create(name=kw['text'], space=request.space)
@@ -141,12 +159,12 @@ def import_url(request):
if ing['ingredient']['text'] != '':
ingredient.food, f_created = Food.objects.get_or_create(
name=ing['ingredient']['text'], space=request.space
name=ing['ingredient']['text'].strip(), space=request.space
)
if ing['unit'] and ing['unit']['text'] != '':
ingredient.unit, u_created = Unit.objects.get_or_create(
name=ing['unit']['text'], space=request.space
name=ing['unit']['text'].strip(), space=request.space
)
# TODO properly handle no_amount recipes
@@ -159,7 +177,7 @@ def import_url(request):
elif isinstance(ing['amount'], float) \
or isinstance(ing['amount'], int):
ingredient.amount = ing['amount']
ingredient.note = ing['note'] if 'note' in ing else ''
ingredient.note = ing['note'].strip() if 'note' in ing else ''
ingredient.save()
step.ingredients.add(ingredient)
@@ -189,7 +207,12 @@ def import_url(request):
return HttpResponse(reverse('view_recipe', args=[recipe.pk]))
return render(request, 'url_import.html', {})
if 'id' in request.GET:
context = {'bookmarklet': request.GET.get('id', '')}
else:
context = {}
return render(request, 'url_import.html', context)
class Object(object):

View File

@@ -1,6 +1,7 @@
import os
from django.contrib import messages
from django.db.models import Q
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse, reverse_lazy
@@ -18,7 +19,7 @@ from cookbook.helper.permission_helper import (GroupRequiredMixin,
group_required)
from cookbook.models import (Comment, Food, Ingredient, Keyword, MealPlan,
MealType, Recipe, RecipeBook, RecipeImport,
Storage, Sync)
Storage, Sync, UserPreference)
from cookbook.provider.dropbox import Dropbox
from cookbook.provider.local import Local
from cookbook.provider.nextcloud import Nextcloud
@@ -45,6 +46,14 @@ def convert_recipe(request, pk):
@group_required('user')
def internal_recipe_update(request, pk):
if request.space.max_recipes != 0 and Recipe.objects.filter(space=request.space).count() > request.space.max_recipes: # TODO move to central helper function
messages.add_message(request, messages.WARNING, _('You have reached the maximum number of recipes for your space.'))
return HttpResponseRedirect(reverse('view_recipe', args=[pk]))
if request.space.max_users != 0 and UserPreference.objects.filter(space=request.space).count() > request.space.max_users:
messages.add_message(request, messages.WARNING, _('You have more users than allowed in your space.'))
return HttpResponseRedirect(reverse('view_recipe', args=[pk]))
recipe_instance = get_object_or_404(Recipe, pk=pk, space=request.space)
return render(
@@ -279,14 +288,15 @@ 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, step__recipe__space=request.space).all()
for i in recipe_ingredients:
i.unit = new_unit
i.save()
with scopes_disabled():
recipe_ingredients = Ingredient.objects.filter(unit=old_unit).filter(Q(step__recipe__space=request.space) | Q(step__recipe__isnull=True)).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!'))
old_unit.delete()
success = True
messages.add_message(request, messages.SUCCESS, _('Units merged!'))
else:
messages.add_message(request, messages.ERROR, _('Cannot merge with the same object!'))
@@ -295,7 +305,7 @@ def edit_ingredients(request):
new_food = food_form.cleaned_data['new_food']
old_food = food_form.cleaned_data['old_food']
if new_food != old_food:
ingredients = Ingredient.objects.filter(food=old_food, step__recipe__space=request.space).all()
ingredients = Ingredient.objects.filter(food=old_food).filter(Q(step__recipe__space=request.space) | Q(step__recipe__isnull=True)).all()
for i in ingredients:
i.food = new_food
i.save()

View File

@@ -3,7 +3,7 @@ import threading
from io import BytesIO
from django.contrib import messages
from django.http import HttpResponseRedirect
from django.http import HttpResponseRedirect, JsonResponse
from django.shortcuts import render
from django.urls import reverse
from django.utils.translation import gettext as _
@@ -18,12 +18,14 @@ from cookbook.integration.domestica import Domestica
from cookbook.integration.mealie import Mealie
from cookbook.integration.mealmaster import MealMaster
from cookbook.integration.nextcloud_cookbook import NextcloudCookbook
from cookbook.integration.openeats import OpenEats
from cookbook.integration.paprika import Paprika
from cookbook.integration.recipekeeper import RecipeKeeper
from cookbook.integration.recettetek import RecetteTek
from cookbook.integration.recipesage import RecipeSage
from cookbook.integration.rezkonv import RezKonv
from cookbook.integration.safron import Safron
from cookbook.models import Recipe, ImportLog
from cookbook.models import Recipe, ImportLog, UserPreference
def get_integration(request, export_type):
@@ -47,16 +49,28 @@ def get_integration(request, export_type):
return Domestica(request, export_type)
if export_type == ImportExportBase.RECIPEKEEPER:
return RecipeKeeper(request, export_type)
if export_type == ImportExportBase.RECETTETEK:
return RecetteTek(request, export_type)
if export_type == ImportExportBase.RECIPESAGE:
return RecipeSage(request, export_type)
if export_type == ImportExportBase.REZKONV:
return RezKonv(request, export_type)
if export_type == ImportExportBase.MEALMASTER:
return MealMaster(request, export_type)
if export_type == ImportExportBase.OPENEATS:
return OpenEats(request, export_type)
@group_required('user')
def import_recipe(request):
if request.space.max_recipes != 0 and Recipe.objects.filter(space=request.space).count() >= request.space.max_recipes: # TODO move to central helper function
messages.add_message(request, messages.WARNING, _('You have reached the maximum number of recipes for your space.'))
return HttpResponseRedirect(reverse('index'))
if request.space.max_users != 0 and UserPreference.objects.filter(space=request.space).count() > request.space.max_users:
messages.add_message(request, messages.WARNING, _('You have more users than allowed in your space.'))
return HttpResponseRedirect(reverse('index'))
if request.method == "POST":
form = ImportForm(request.POST, request.FILES)
if form.is_valid():
@@ -71,9 +85,15 @@ def import_recipe(request):
t.setDaemon(True)
t.start()
return HttpResponseRedirect(reverse('view_import_response', args=[il.pk]))
return JsonResponse({'import_id': [il.pk]})
except NotImplementedError:
messages.add_message(request, messages.ERROR, _('Importing is not implemented for this provider'))
return JsonResponse(
{
'error': True,
'msg': _('Importing is not implemented for this provider')
},
status=400
)
else:
form = ImportForm()

View File

@@ -30,7 +30,7 @@ def keyword(request):
@group_required('admin')
def sync_log(request):
table = ImportLogTable(
SyncLog.objects.filter(space=request.space).all().order_by('-created_at')
SyncLog.objects.filter(sync__space=request.space).all().order_by('-created_at')
)
RequestConfig(request, paginate={'per_page': 25}).configure(table)

View File

@@ -1,7 +1,11 @@
import re
from datetime import datetime
from datetime import datetime, timedelta
from html import escape
from smtplib import SMTPException
from django.contrib import messages
from django.contrib.auth.models import Group
from django.core.mail import send_mail, BadHeaderError
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse, reverse_lazy
@@ -13,7 +17,7 @@ from cookbook.forms import (ImportRecipeForm, InviteLinkForm, KeywordForm,
from cookbook.helper.permission_helper import (GroupRequiredMixin,
group_required)
from cookbook.models import (InviteLink, Keyword, MealPlan, MealType, Recipe,
RecipeBook, RecipeImport, ShareLink, Step)
RecipeBook, RecipeImport, ShareLink, Step, UserPreference)
from cookbook.views.edit import SpaceFormMixing
@@ -24,6 +28,14 @@ class RecipeCreate(GroupRequiredMixin, CreateView):
fields = ('name',)
def form_valid(self, form):
if self.request.space.max_recipes != 0 and Recipe.objects.filter(space=self.request.space).count() >= self.request.space.max_recipes: # TODO move to central helper function
messages.add_message(self.request, messages.WARNING, _('You have reached the maximum number of recipes for your space.'))
return HttpResponseRedirect(reverse('index'))
if self.request.space.max_users != 0 and UserPreference.objects.filter(space=self.request.space).count() > self.request.space.max_users:
messages.add_message(self.request, messages.WARNING, _('You have more users than allowed in your space.'))
return HttpResponseRedirect(reverse('index'))
obj = form.save(commit=False)
obj.created_by = self.request.user
obj.space = self.request.space
@@ -154,7 +166,8 @@ class MealPlanCreate(GroupRequiredMixin, CreateView, SpaceFormMixing):
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, space=self.request.space).all()
form.fields['meal_type'].queryset = MealType.objects.filter(created_by=self.request.user,
space=self.request.space).all()
return form
def get_initial(self):
@@ -207,7 +220,32 @@ class InviteLinkCreate(GroupRequiredMixin, CreateView):
obj.created_by = self.request.user
obj.space = self.request.space
obj.save()
return HttpResponseRedirect(reverse('list_invite_link'))
if obj.email:
try:
if InviteLink.objects.filter(space=self.request.space, created_at__gte=datetime.now() - timedelta(hours=4)).count() < 20:
message = _('Hello') + '!\n\n' + _('You have been invited by ') + escape(self.request.user.username)
message += _(' to join their Tandoor Recipes space ') + escape(self.request.space.name) + '.\n\n'
message += _('Click the following link to activate your account: ') + self.request.build_absolute_uri(reverse('view_invite', args=[str(obj.uuid)])) + '\n\n'
message += _('If the link does not work use the following code to manually join the space: ') + str(obj.uuid) + '\n\n'
message += _('The invitation is valid until ') + str(obj.valid_until) + '\n\n'
message += _('Tandoor Recipes is an Open Source recipe manager. Check it out on GitHub ') + 'https://github.com/vabene1111/recipes/'
send_mail(
_('Tandoor Recipes Invite'),
message,
None,
[obj.email],
fail_silently=False,
)
messages.add_message(self.request, messages.SUCCESS,
_('Invite link successfully send to user.'))
else:
messages.add_message(self.request, messages.ERROR,
_('You have send to many emails, please share the link manually or wait a few hours.'))
except (SMTPException, BadHeaderError, TimeoutError):
messages.add_message(self.request, messages.ERROR, _('Email to user could not be send, please share link manually.'))
return HttpResponseRedirect(reverse('view_space'))
def get_context_data(self, **kwargs):
context = super(InviteLinkCreate, self).get_context_data(**kwargs)
@@ -218,3 +256,9 @@ class InviteLinkCreate(GroupRequiredMixin, CreateView):
kwargs = super().get_form_kwargs()
kwargs.update({'user': self.request.user})
return kwargs
def get_initial(self):
return dict(
space=self.request.space,
group=Group.objects.get(name='user')
)

View File

@@ -3,15 +3,17 @@ import re
from datetime import datetime
from uuid import UUID
from allauth.account.forms import SignupForm
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth.models import Group
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 Avg, Q
from django.db.models import Avg, Q, Sum
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render, redirect
from django.urls import reverse, reverse_lazy
@@ -24,13 +26,14 @@ from rest_framework.authtoken.models import Token
from cookbook.filters import RecipeFilter
from cookbook.forms import (CommentForm, Recipe, RecipeBookEntryForm, User,
UserCreateForm, UserNameForm, UserPreference,
UserPreferenceForm)
UserPreferenceForm, SpaceJoinForm, SpaceCreateForm, AllAuthSignupForm)
from cookbook.helper.permission_helper import group_required, share_link_valid, has_group_permission
from cookbook.models import (Comment, CookLog, InviteLink, MealPlan,
RecipeBook, RecipeBookEntry, ViewLog, ShoppingList, Space)
RecipeBook, RecipeBookEntry, ViewLog, ShoppingList, Space, Keyword, RecipeImport, Unit,
Food, UserFile)
from cookbook.tables import (CookLogTable, RecipeTable, RecipeTableSmall,
ViewLogTable)
from recipes.settings import DEMO
ViewLogTable, InviteLinkTable)
from cookbook.views.data import Object
from recipes.version import BUILD_REF, VERSION_NUMBER
@@ -99,18 +102,50 @@ def no_groups(request):
return render(request, 'no_groups_info.html')
@login_required
def no_space(request):
if settings.SOCIAL_DEFAULT_ACCESS:
request.user.userpreference.space = Space.objects.first()
request.user.userpreference.save()
request.user.groups.add(Group.objects.get(name=settings.SOCIAL_DEFAULT_GROUP))
if request.user.userpreference.space:
return HttpResponseRedirect(reverse('index'))
return render(request, 'no_space_info.html')
if request.POST:
create_form = SpaceCreateForm(request.POST, prefix='create')
join_form = SpaceJoinForm(request.POST, prefix='join')
if create_form.is_valid():
created_space = Space.objects.create(
name=create_form.cleaned_data['name'],
created_by=request.user,
allow_files=settings.SPACE_DEFAULT_FILES,
max_recipes=settings.SPACE_DEFAULT_MAX_RECIPES,
max_users=settings.SPACE_DEFAULT_MAX_USERS,
)
request.user.userpreference.space = created_space
request.user.userpreference.save()
request.user.groups.add(Group.objects.filter(name='admin').get())
messages.add_message(request, messages.SUCCESS, _('You have successfully created your own recipe space. Start by adding some recipes or invite other people to join you.'))
return HttpResponseRedirect(reverse('index'))
if join_form.is_valid():
return HttpResponseRedirect(reverse('view_invite', args=[join_form.cleaned_data['token']]))
else:
if settings.SOCIAL_DEFAULT_ACCESS:
request.user.userpreference.space = Space.objects.first()
request.user.userpreference.save()
request.user.groups.add(Group.objects.get(name=settings.SOCIAL_DEFAULT_GROUP))
return HttpResponseRedirect(reverse('index'))
if 'signup_token' in request.session:
return HttpResponseRedirect(reverse('view_invite', args=[request.session.pop('signup_token', '')]))
create_form = SpaceCreateForm()
join_form = SpaceJoinForm()
return render(request, 'no_space_info.html', {'create_form': create_form, 'join_form': join_form})
def no_perm(request):
if not request.user.is_authenticated:
return HttpResponseRedirect(reverse('index'))
messages.add_message(request, messages.ERROR, _('You are not logged in and therefore cannot view this page!'))
return HttpResponseRedirect(reverse('account_login') + '?next=' + request.GET.get('next', '/search/'))
return render(request, 'no_perm_info.html')
@@ -196,6 +231,20 @@ def meal_plan(request):
return render(request, 'meal_plan.html', {})
@group_required('user')
def supermarket(request):
return render(request, 'supermarket.html', {})
@group_required('user')
def files(request):
try:
current_file_size_mb = UserFile.objects.filter(space=request.space).aggregate(Sum('file_size_kb'))['file_size_kb__sum'] / 1000
except TypeError:
current_file_size_mb = 0
return render(request, 'files.html', {'current_file_size_mb': current_file_size_mb, 'max_file_size_mb': request.space.max_file_storage_mb})
@group_required('user')
def meal_plan_entry(request, pk):
plan = MealPlan.objects.filter(space=request.space).get(pk=pk)
@@ -215,9 +264,7 @@ def meal_plan_entry(request, pk):
@group_required('user')
def latest_shopping_list(request):
sl = ShoppingList.objects.filter(Q(created_by=request.user) | Q(shared=request.user)).filter(finished=False,
space=request.space).order_by(
'-created_at').first()
sl = ShoppingList.objects.filter(Q(created_by=request.user) | Q(shared=request.user)).filter(finished=False, pace=request.space).order_by('-created_at').first()
if sl:
return HttpResponseRedirect(reverse('view_shopping', kwargs={'pk': sl.pk}) + '?edit=true')
@@ -227,10 +274,10 @@ def latest_shopping_list(request):
@group_required('user')
def shopping_list(request, pk=None):
raw_list = request.GET.getlist('r')
html_list = request.GET.getlist('r')
recipes = []
for r in raw_list:
for r in html_list:
r = r.replace('[', '').replace(']', '')
if re.match(r'^([0-9])+,([0-9])+[.]*([0-9])*$', r):
rid, multiplier = r.split(',')
@@ -244,7 +291,7 @@ def shopping_list(request, pk=None):
@group_required('guest')
def user_settings(request):
if DEMO:
if request.space.demo:
messages.add_message(request, messages.ERROR, _('This feature is not available in the demo version!'))
return redirect('index')
@@ -274,16 +321,16 @@ def user_settings(request):
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: # noqa: E501
up.shopping_auto_sync = settings.SHOPPING_MIN_AUTOSYNC_INTERVAL # noqa: E501
if up.shopping_auto_sync < settings.SHOPPING_MIN_AUTOSYNC_INTERVAL:
up.shopping_auto_sync = settings.SHOPPING_MIN_AUTOSYNC_INTERVAL
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'] # noqa: E501
request.user.last_name = user_name_form.cleaned_data['last_name'] # noqa: E501
request.user.first_name = user_name_form.cleaned_data['first_name']
request.user.last_name = user_name_form.cleaned_data['last_name']
request.user.save()
if 'password_form' in request.POST:
@@ -304,7 +351,7 @@ def user_settings(request):
'preference_form': preference_form,
'user_name_form': user_name_form,
'password_form': password_form,
'api_token': api_token
'api_token': api_token,
})
@@ -380,7 +427,7 @@ def setup(request):
return render(request, 'setup.html', {'form': form})
def signup(request, token):
def invite_link(request, token):
with scopes_disabled():
try:
token = UUID(token, version=4)
@@ -389,44 +436,83 @@ def signup(request, token):
return HttpResponseRedirect(reverse('index'))
if link := InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None, uuid=token).first():
if request.method == 'POST':
updated_request = request.POST.copy()
if link.username != '':
updated_request.update({'name': link.username})
if request.user.is_authenticated:
if request.user.userpreference.space:
messages.add_message(request, messages.WARNING, _('You are already member of a space and therefore cannot join this one.'))
return HttpResponseRedirect(reverse('index'))
form = UserCreateForm(updated_request)
link.used_by = request.user
link.save()
request.user.groups.clear()
request.user.groups.add(link.group)
if form.is_valid():
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)
user.set_password(form.cleaned_data['password'])
user.save()
messages.add_message(request, messages.SUCCESS, _('User has been created, please login!'))
request.user.userpreference.space = link.space
request.user.userpreference.save()
link.used_by = user
link.save()
user.groups.add(link.group)
user.userpreference.space = link.space
user.userpreference.save()
return HttpResponseRedirect(reverse('account_login'))
except ValidationError as e:
for m in e:
form.add_error('password', m)
messages.add_message(request, messages.SUCCESS, _('Successfully joined space.'))
return HttpResponseRedirect(reverse('index'))
else:
form = UserCreateForm()
request.session['signup_token'] = str(token)
return HttpResponseRedirect(reverse('account_signup'))
if link.username != '':
form.fields['name'].initial = link.username
form.fields['name'].disabled = True
return render(request, 'account/signup.html', {'form': form, 'link': link})
messages.add_message(request, messages.ERROR, _('Invite Link not valid or already used!'))
return HttpResponseRedirect(reverse('index'))
messages.add_message(request, messages.ERROR, _('Invite Link not valid or already used!'))
return HttpResponseRedirect(reverse('index'))
# TODO deprecated with 0.16.2 remove at some point
def signup(request, token):
return HttpResponseRedirect(reverse('view_invite', args=[token]))
@group_required('admin')
def space(request):
space_users = UserPreference.objects.filter(space=request.space).all()
counts = Object()
counts.recipes = Recipe.objects.filter(space=request.space).count()
counts.keywords = Keyword.objects.filter(space=request.space).count()
counts.recipe_import = RecipeImport.objects.filter(space=request.space).count()
counts.units = Unit.objects.filter(space=request.space).count()
counts.ingredients = Food.objects.filter(space=request.space).count()
counts.comments = Comment.objects.filter(recipe__space=request.space).count()
counts.recipes_internal = Recipe.objects.filter(internal=True, space=request.space).count()
counts.recipes_external = counts.recipes - counts.recipes_internal
counts.recipes_no_keyword = Recipe.objects.filter(keywords=None, space=request.space).count()
invite_links = InviteLinkTable(InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None, space=request.space).all())
RequestConfig(request, paginate={'per_page': 25}).configure(invite_links)
return render(request, 'space.html', {'space_users': space_users, 'counts': counts, 'invite_links': invite_links})
# TODO super hacky and quick solution, safe but needs rework
# TODO move group settings to space to prevent permissions from one space to move to another
@group_required('admin')
def space_change_member(request, user_id, space_id, group):
m_space = get_object_or_404(Space, pk=space_id)
m_user = get_object_or_404(User, pk=user_id)
if request.user == m_space.created_by and m_user != m_space.created_by:
if m_user.userpreference.space == m_space:
if group == 'admin':
m_user.groups.clear()
m_user.groups.add(Group.objects.get(name='admin'))
return HttpResponseRedirect(reverse('view_space'))
if group == 'user':
m_user.groups.clear()
m_user.groups.add(Group.objects.get(name='user'))
return HttpResponseRedirect(reverse('view_space'))
if group == 'guest':
m_user.groups.clear()
m_user.groups.add(Group.objects.get(name='guest'))
return HttpResponseRedirect(reverse('view_space'))
if group == 'remove':
m_user.groups.clear()
m_user.userpreference.space = None
m_user.userpreference.save()
return HttpResponseRedirect(reverse('view_space'))
return HttpResponseRedirect(reverse('view_space'))
def markdown_info(request):