mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-06 22:58:19 -05:00
Merge branch 'develop' into importer-reciepekeeper
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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')
|
||||
)
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user