mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-01 04:10:06 -05:00
Merge branch 'develop' into feature/vue3
# Conflicts: # cookbook/views/api.py # cookbook/views/views.py # requirements.txt
This commit is contained in:
@@ -89,12 +89,13 @@ class ImportExportBase(forms.Form):
|
||||
COOKMATE = 'COOKMATE'
|
||||
REZEPTSUITEDE = 'REZEPTSUITEDE'
|
||||
PDF = 'PDF'
|
||||
GOURMET = 'GOURMET'
|
||||
|
||||
type = forms.ChoiceField(choices=((DEFAULT, _('Default')), (PAPRIKA, 'Paprika'), (NEXTCLOUD, 'Nextcloud Cookbook'), (MEALIE, 'Mealie'), (CHOWDOWN, 'Chowdown'),
|
||||
(SAFFRON, 'Saffron'), (CHEFTAP, 'ChefTap'), (PEPPERPLATE, 'Pepperplate'), (RECETTETEK, 'RecetteTek'), (RECIPESAGE, 'Recipe Sage'),
|
||||
(DOMESTICA, 'Domestica'), (MEALMASTER, 'MealMaster'), (REZKONV, 'RezKonv'), (OPENEATS, 'Openeats'), (RECIPEKEEPER, 'Recipe Keeper'),
|
||||
(PLANTOEAT, 'Plantoeat'), (COOKBOOKAPP, 'CookBookApp'), (COPYMETHAT, 'CopyMeThat'), (PDF, 'PDF'), (MELARECIPES, 'Melarecipes'),
|
||||
(COOKMATE, 'Cookmate'), (REZEPTSUITEDE, 'Recipesuite.de')))
|
||||
(COOKMATE, 'Cookmate'), (REZEPTSUITEDE, 'Recipesuite.de'), (GOURMET, 'Gourmet')))
|
||||
|
||||
|
||||
class MultipleFileInput(forms.ClearableFileInput):
|
||||
|
||||
211
cookbook/integration/gourmet.py
Normal file
211
cookbook/integration/gourmet.py
Normal file
@@ -0,0 +1,211 @@
|
||||
import base64
|
||||
from io import BytesIO
|
||||
from lxml import etree
|
||||
import requests
|
||||
from pathlib import Path
|
||||
|
||||
from bs4 import BeautifulSoup, Tag
|
||||
|
||||
from cookbook.helper.HelperFunctions import validate_import_url
|
||||
from cookbook.helper.ingredient_parser import IngredientParser
|
||||
from cookbook.helper.recipe_url_import import parse_servings, parse_servings_text, parse_time, iso_duration_to_minutes
|
||||
from cookbook.integration.integration import Integration
|
||||
from cookbook.models import Ingredient, Recipe, Step, Keyword
|
||||
from recipe_scrapers import scrape_html
|
||||
|
||||
|
||||
class Gourmet(Integration):
|
||||
|
||||
def split_recipe_file(self, file):
|
||||
encoding = 'utf-8'
|
||||
byte_string = file.read()
|
||||
text_obj = byte_string.decode(encoding, errors="ignore")
|
||||
soup = BeautifulSoup(text_obj, "html.parser")
|
||||
return soup.find_all("div", {"class": "recipe"})
|
||||
|
||||
def get_ingredients_recursive(self, step, ingredients, ingredient_parser):
|
||||
if isinstance(ingredients, Tag):
|
||||
for ingredient in ingredients.children:
|
||||
if not isinstance(ingredient, Tag):
|
||||
continue
|
||||
|
||||
if ingredient.name in ["li"]:
|
||||
step_name = "".join(ingredient.findAll(text=True, recursive=False)).strip().rstrip(":")
|
||||
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
is_header=True,
|
||||
note=step_name[:256],
|
||||
original_text=step_name,
|
||||
space=self.request.space,
|
||||
))
|
||||
next_ingrediets = ingredient.find("ul", {"class": "ing"})
|
||||
self.get_ingredients_recursive(step, next_ingrediets, ingredient_parser)
|
||||
|
||||
else:
|
||||
try:
|
||||
amount, unit, food, note = ingredient_parser.parse(ingredient.text.strip())
|
||||
f = ingredient_parser.get_food(food)
|
||||
u = ingredient_parser.get_unit(unit)
|
||||
step.ingredients.add(
|
||||
Ingredient.objects.create(
|
||||
food=f,
|
||||
unit=u,
|
||||
amount=amount,
|
||||
note=note,
|
||||
original_text=ingredient.text.strip(),
|
||||
space=self.request.space,
|
||||
)
|
||||
)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def get_recipe_from_file(self, file):
|
||||
# 'file' comes is as a beautifulsoup object
|
||||
|
||||
source_url = None
|
||||
for item in file.find_all('a'):
|
||||
if item.has_attr('href'):
|
||||
source_url = item.get("href")
|
||||
break
|
||||
|
||||
name = file.find("p", {"class": "title"}).find("span", {"itemprop": "name"}).text.strip()
|
||||
|
||||
recipe = Recipe.objects.create(
|
||||
name=name[:128],
|
||||
source_url=source_url,
|
||||
created_by=self.request.user,
|
||||
internal=True,
|
||||
space=self.request.space,
|
||||
)
|
||||
|
||||
for category in file.find_all("span", {"itemprop": "recipeCategory"}):
|
||||
keyword, created = Keyword.objects.get_or_create(name=category.text, space=self.request.space)
|
||||
recipe.keywords.add(keyword)
|
||||
|
||||
try:
|
||||
recipe.servings = parse_servings(file.find("span", {"itemprop": "recipeYield"}).text.strip())
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
try:
|
||||
prep_time = file.find("span", {"itemprop": "prepTime"}).text.strip().split()
|
||||
prep_time[0] = prep_time[0].replace(',', '.')
|
||||
if prep_time[1].lower() in ['stunde', 'stunden', 'hour', 'hours']:
|
||||
prep_time_min = int(float(prep_time[0]) * 60)
|
||||
elif prep_time[1].lower() in ['tag', 'tage', 'day', 'days']:
|
||||
prep_time_min = int(float(prep_time[0]) * 60 * 24)
|
||||
else:
|
||||
prep_time_min = int(prep_time[0])
|
||||
recipe.waiting_time = prep_time_min
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
try:
|
||||
cook_time = file.find("span", {"itemprop": "cookTime"}).text.strip().split()
|
||||
cook_time[0] = cook_time[0].replace(',', '.')
|
||||
if cook_time[1].lower() in ['stunde', 'stunden', 'hour', 'hours']:
|
||||
cook_time_min = int(float(cook_time[0]) * 60)
|
||||
elif cook_time[1].lower() in ['tag', 'tage', 'day', 'days']:
|
||||
cook_time_min = int(float(cook_time[0]) * 60 * 24)
|
||||
else:
|
||||
cook_time_min = int(cook_time[0])
|
||||
|
||||
recipe.working_time = cook_time_min
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
for cuisine in file.find_all('span', {'itemprop': 'recipeCuisine'}):
|
||||
cuisine_name = cuisine.text
|
||||
keyword = Keyword.objects.get_or_create(space=self.request.space, name=cuisine_name)
|
||||
if len(keyword):
|
||||
recipe.keywords.add(keyword[0])
|
||||
|
||||
for category in file.find_all('span', {'itemprop': 'recipeCategory'}):
|
||||
category_name = category.text
|
||||
keyword = Keyword.objects.get_or_create(space=self.request.space, name=category_name)
|
||||
if len(keyword):
|
||||
recipe.keywords.add(keyword[0])
|
||||
|
||||
step = Step.objects.create(
|
||||
instruction='',
|
||||
space=self.request.space,
|
||||
show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
|
||||
)
|
||||
|
||||
ingredient_parser = IngredientParser(self.request, True)
|
||||
|
||||
ingredients = file.find("ul", {"class": "ing"})
|
||||
self.get_ingredients_recursive(step, ingredients, ingredient_parser)
|
||||
|
||||
instructions = file.find("div", {"class": "instructions"})
|
||||
if isinstance(instructions, Tag):
|
||||
for instruction in instructions.children:
|
||||
if not isinstance(instruction, Tag) or instruction.text == "":
|
||||
continue
|
||||
if instruction.name == "h3":
|
||||
if step.instruction:
|
||||
step.save()
|
||||
recipe.steps.add(step)
|
||||
step = Step.objects.create(
|
||||
instruction='',
|
||||
space=self.request.space,
|
||||
)
|
||||
|
||||
step.name = instruction.text.strip()[:128]
|
||||
else:
|
||||
if instruction.name == "div":
|
||||
for instruction_step in instruction.children:
|
||||
for br in instruction_step.find_all("br"):
|
||||
br.replace_with("\n")
|
||||
step.instruction += instruction_step.text.strip() + ' \n\n'
|
||||
|
||||
notes = file.find("div", {"class": "modifications"})
|
||||
if notes:
|
||||
for n in notes.children:
|
||||
if n.text == "":
|
||||
continue
|
||||
if n.name == "h3":
|
||||
step.instruction += f'*{n.text.strip()}:* \n\n'
|
||||
else:
|
||||
for br in n.find_all("br"):
|
||||
br.replace_with("\n")
|
||||
|
||||
step.instruction += '*' + n.text.strip() + '* \n\n'
|
||||
|
||||
description = ''
|
||||
try:
|
||||
description = file.find("div", {"id": "description"}).text.strip()
|
||||
except AttributeError:
|
||||
pass
|
||||
if len(description) <= 512:
|
||||
recipe.description = description
|
||||
else:
|
||||
recipe.description = description[:480] + ' ... (full description below)'
|
||||
step.instruction += '*Description:* \n\n*' + description + '* \n\n'
|
||||
|
||||
step.save()
|
||||
recipe.steps.add(step)
|
||||
|
||||
# import the Primary recipe image that is stored in the Zip
|
||||
try:
|
||||
image_path = file.find("img").get("src")
|
||||
image_filename = image_path.split("\\")[1]
|
||||
|
||||
for f in self.import_zip.filelist:
|
||||
zip_file_name = Path(f.filename).name
|
||||
if image_filename == zip_file_name:
|
||||
image_file = self.import_zip.read(f)
|
||||
image_bytes = BytesIO(image_file)
|
||||
self.import_recipe_image(recipe, image_bytes, filetype='.jpeg')
|
||||
break
|
||||
except Exception as e:
|
||||
print(recipe.name, ': failed to import image ', str(e))
|
||||
|
||||
recipe.save()
|
||||
return recipe
|
||||
|
||||
def get_files_from_recipes(self, recipes, el, cookie):
|
||||
raise NotImplementedError('Method not implemented in storage integration')
|
||||
|
||||
def get_file_from_recipe(self, recipe):
|
||||
raise NotImplementedError('Method not implemented in storage integration')
|
||||
@@ -153,6 +153,19 @@ class Integration:
|
||||
il.total_recipes = len(new_file_list)
|
||||
file_list = new_file_list
|
||||
|
||||
if isinstance(self, cookbook.integration.gourmet.Gourmet):
|
||||
self.import_zip = import_zip
|
||||
new_file_list = []
|
||||
for file in file_list:
|
||||
if file.file_size == 0:
|
||||
next
|
||||
if file.filename.startswith("index.htm"):
|
||||
next
|
||||
if file.filename.endswith(".htm"):
|
||||
new_file_list += self.split_recipe_file(BytesIO(import_zip.read(file.filename)))
|
||||
il.total_recipes = len(new_file_list)
|
||||
file_list = new_file_list
|
||||
|
||||
for z in file_list:
|
||||
try:
|
||||
if not hasattr(z, 'filename') or isinstance(z, Tag):
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-08-01 15:04+0200\n"
|
||||
"PO-Revision-Date: 2024-08-27 07:58+0000\n"
|
||||
"Last-Translator: Known Rabbit <opensource@rabit.pw>\n"
|
||||
"PO-Revision-Date: 2024-11-04 10:29+0000\n"
|
||||
"Last-Translator: Johnny Ip <ip.iohnny@gmail.com>\n"
|
||||
"Language-Team: Chinese (Simplified) <http://translate.tandoor.dev/projects/"
|
||||
"tandoor/recipes-backend/zh_Hans/>\n"
|
||||
"Language: zh_CN\n"
|
||||
@@ -1988,9 +1988,11 @@ msgid ""
|
||||
" your installation.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"<b>不推荐</b> 使用 gunicorn/python 提供媒体文件。\n"
|
||||
" 请按照 <a href=\"https://github.com/vabene1111/recipes/releases/"
|
||||
"tag/0.8.1\">这里</a> 描述的步骤操作更新安装。\n"
|
||||
"<b>不推荐</b> 使用 gunicorn/python 提供媒体文件!\n"
|
||||
" 请按照\n"
|
||||
" <a href=\"https://github.com/vabene1111/recipes/releases/tag/0.8."
|
||||
"1\">这里</a> 描述的步骤\n"
|
||||
" 操作更新安装。\n"
|
||||
" "
|
||||
|
||||
#: .\cookbook\templates\system.html:55 .\cookbook\templates\system.html:70
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -82,9 +82,9 @@
|
||||
{% else %}
|
||||
{% trans 'Everything is fine!' %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
<h4 class="mt-3">{% trans 'Allowed Hosts' %} <span
|
||||
class="badge badge-{% if '*' in allowed_hosts %}warning{% else %}success{% endif %}">{% if '*' in allowed_hosts %}
|
||||
class="badge badge-{% if '*' in allowed_hosts %}warning{% else %}success{% endif %}">{% if '*' in allowed_hosts %}
|
||||
{% trans 'Warning' %}{% else %}{% trans 'Ok' %}{% endif %}</span></h4>
|
||||
{% if debug %}
|
||||
{% blocktrans %}
|
||||
@@ -176,6 +176,33 @@
|
||||
{#{% endfor %}#}
|
||||
{# </textarea>#}
|
||||
|
||||
<h4 class="mt-3">API Stats</h4>
|
||||
<h6 >Space Stats</h6>
|
||||
<table class="table table-bordered table-striped">
|
||||
{% for r in api_space_stats %}
|
||||
<tr>
|
||||
{% for c in r %}
|
||||
<td>
|
||||
{{ c }}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
<h6 >Endpoint Stats</h6>
|
||||
<table class="table table-bordered table-striped">
|
||||
{% for r in api_stats %}
|
||||
<tr>
|
||||
{% for c in r %}
|
||||
<td>
|
||||
{{ c }}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
<h4 class="mt-3">Debug</h4>
|
||||
<textarea class="form-control" rows="20">
|
||||
Gunicorn Media: {{ gunicorn_media }}
|
||||
|
||||
@@ -13,6 +13,7 @@ from urllib.parse import unquote
|
||||
from zipfile import ZipFile
|
||||
|
||||
import PIL.Image
|
||||
import redis
|
||||
import requests
|
||||
from PIL import UnidentifiedImageError
|
||||
from django.contrib import messages
|
||||
@@ -29,6 +30,7 @@ from django.http import FileResponse, HttpResponse, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.datetime_safe import date
|
||||
from django.utils.translation import gettext as _
|
||||
from django_scopes import scopes_disabled
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
@@ -109,6 +111,44 @@ from recipes.settings import DRF_THROTTLE_RECIPE_URL_IMPORT, FDC_API_KEY, GOOGLE
|
||||
DateExample = OpenApiExample('Date Format', value='1972-12-05', request_only=True)
|
||||
BeforeDateExample = OpenApiExample('Before Date Format', value='-1972-12-05', request_only=True)
|
||||
|
||||
class LoggingMixin(object):
|
||||
"""
|
||||
logs request counts to redis cache total/per user/
|
||||
"""
|
||||
|
||||
def initial(self, request, *args, **kwargs):
|
||||
super(LoggingMixin, self).initial(request, *args, **kwargs)
|
||||
|
||||
if settings.REDIS_HOST:
|
||||
d = date.today().isoformat()
|
||||
space = request.space
|
||||
endpoint = request.resolver_match.url_name
|
||||
|
||||
r = redis.StrictRedis(
|
||||
host=settings.REDIS_HOST,
|
||||
port=settings.REDIS_PORT,
|
||||
username=settings.REDIS_USERNAME,
|
||||
password=settings.REDIS_PASSWORD,
|
||||
db=settings.REDIS_DATABASES['STATS'],
|
||||
)
|
||||
|
||||
pipe = r.pipeline()
|
||||
|
||||
# Global and daily tallies for all URLs.
|
||||
pipe.incr('api:request-count')
|
||||
pipe.incr(f'api:request-count:{d}')
|
||||
|
||||
# Use a sorted set to store the user stats, with the score representing
|
||||
# the number of queries the user made total or on a given day.
|
||||
pipe.zincrby(f'api:space-request-count', 1, space.pk)
|
||||
pipe.zincrby(f'api:space-request-count:{d}', 1, space.pk)
|
||||
|
||||
# Use a sorted set to store all the endpoints with score representing
|
||||
# the number of queries the endpoint received total or on a given day.
|
||||
pipe.zincrby(f'api:endpoint-request-count', 1, endpoint)
|
||||
pipe.zincrby(f'api:endpoint-request-count:{d}', 1, endpoint)
|
||||
|
||||
pipe.execute()
|
||||
|
||||
@extend_schema_view(list=extend_schema(parameters=[
|
||||
OpenApiParameter(name='query', description='lookup if query string is contained within the name, case insensitive',
|
||||
@@ -423,7 +463,7 @@ class TreeMixin(MergeMixin, FuzzyFilterMixin):
|
||||
@extend_schema_view(list=extend_schema(parameters=[
|
||||
OpenApiParameter(name='filter_list', description='User IDs, repeat for multiple', type=str, many=True),
|
||||
]))
|
||||
class UserViewSet(viewsets.ModelViewSet):
|
||||
class UserViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
queryset = User.objects
|
||||
serializer_class = UserSerializer
|
||||
permission_classes = [CustomUserPermission & CustomTokenHasReadWriteScope]
|
||||
@@ -442,7 +482,7 @@ class UserViewSet(viewsets.ModelViewSet):
|
||||
return queryset
|
||||
|
||||
|
||||
class GroupViewSet(viewsets.ModelViewSet):
|
||||
class GroupViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
queryset = Group.objects.all()
|
||||
serializer_class = GroupSerializer
|
||||
permission_classes = [CustomIsAdmin & CustomTokenHasReadWriteScope]
|
||||
@@ -450,7 +490,7 @@ class GroupViewSet(viewsets.ModelViewSet):
|
||||
http_method_names = ['get', ]
|
||||
|
||||
|
||||
class SpaceViewSet(viewsets.ModelViewSet):
|
||||
class SpaceViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
queryset = Space.objects
|
||||
serializer_class = SpaceSerializer
|
||||
permission_classes = [IsReadOnlyDRF & CustomIsGuest | CustomIsOwner & CustomIsAdmin & CustomTokenHasReadWriteScope]
|
||||
@@ -471,7 +511,7 @@ class SpaceViewSet(viewsets.ModelViewSet):
|
||||
@extend_schema_view(list=extend_schema(parameters=[
|
||||
OpenApiParameter(name='internal_note', description='text field to store information about the invite link', type=str),
|
||||
]))
|
||||
class UserSpaceViewSet(viewsets.ModelViewSet):
|
||||
class UserSpaceViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
queryset = UserSpace.objects
|
||||
serializer_class = UserSpaceSerializer
|
||||
permission_classes = [(CustomIsSpaceOwner | CustomIsOwnerReadOnly) & CustomTokenHasReadWriteScope]
|
||||
@@ -494,7 +534,7 @@ class UserSpaceViewSet(viewsets.ModelViewSet):
|
||||
return self.queryset.filter(user=self.request.user, space=self.request.space)
|
||||
|
||||
|
||||
class UserPreferenceViewSet(viewsets.ModelViewSet):
|
||||
class UserPreferenceViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
queryset = UserPreference.objects
|
||||
serializer_class = UserPreferenceSerializer
|
||||
permission_classes = [CustomIsOwner & CustomTokenHasReadWriteScope]
|
||||
@@ -506,7 +546,7 @@ class UserPreferenceViewSet(viewsets.ModelViewSet):
|
||||
return self.queryset.filter(user=self.request.user)
|
||||
|
||||
|
||||
class StorageViewSet(viewsets.ModelViewSet):
|
||||
class StorageViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
# TODO handle delete protect error and adjust test
|
||||
queryset = Storage.objects
|
||||
serializer_class = StorageSerializer
|
||||
@@ -517,7 +557,7 @@ class StorageViewSet(viewsets.ModelViewSet):
|
||||
return self.queryset.filter(space=self.request.space)
|
||||
|
||||
|
||||
class ConnectorConfigConfigViewSet(viewsets.ModelViewSet):
|
||||
class ConnectorConfigConfigViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
queryset = ConnectorConfig.objects
|
||||
serializer_class = ConnectorConfigConfigSerializer
|
||||
permission_classes = [CustomIsAdmin & CustomTokenHasReadWriteScope]
|
||||
@@ -527,7 +567,7 @@ class ConnectorConfigConfigViewSet(viewsets.ModelViewSet):
|
||||
return self.queryset.filter(space=self.request.space)
|
||||
|
||||
|
||||
class SyncViewSet(viewsets.ModelViewSet):
|
||||
class SyncViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
queryset = Sync.objects
|
||||
serializer_class = SyncSerializer
|
||||
permission_classes = [CustomIsAdmin & CustomTokenHasReadWriteScope]
|
||||
@@ -537,7 +577,7 @@ class SyncViewSet(viewsets.ModelViewSet):
|
||||
return self.queryset.filter(space=self.request.space)
|
||||
|
||||
|
||||
class SyncLogViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
class SyncLogViewSet(LoggingMixin, viewsets.ReadOnlyModelViewSet):
|
||||
queryset = SyncLog.objects
|
||||
serializer_class = SyncLogSerializer
|
||||
permission_classes = [CustomIsAdmin & CustomTokenHasReadWriteScope]
|
||||
@@ -547,7 +587,7 @@ class SyncLogViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
return self.queryset.filter(sync__space=self.request.space)
|
||||
|
||||
|
||||
class SupermarketViewSet(StandardFilterModelViewSet):
|
||||
class SupermarketViewSet(LoggingMixin, StandardFilterModelViewSet):
|
||||
queryset = Supermarket.objects
|
||||
serializer_class = SupermarketSerializer
|
||||
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
|
||||
@@ -559,7 +599,7 @@ class SupermarketViewSet(StandardFilterModelViewSet):
|
||||
|
||||
|
||||
# TODO does supermarket category have settings to support fuzzy filtering and/or merge?
|
||||
class SupermarketCategoryViewSet(FuzzyFilterMixin, MergeMixin):
|
||||
class SupermarketCategoryViewSet(LoggingMixin, FuzzyFilterMixin, MergeMixin):
|
||||
queryset = SupermarketCategory.objects
|
||||
model = SupermarketCategory
|
||||
serializer_class = SupermarketCategorySerializer
|
||||
@@ -571,7 +611,7 @@ class SupermarketCategoryViewSet(FuzzyFilterMixin, MergeMixin):
|
||||
return super().get_queryset()
|
||||
|
||||
|
||||
class SupermarketCategoryRelationViewSet(StandardFilterModelViewSet):
|
||||
class SupermarketCategoryRelationViewSet(LoggingMixin, StandardFilterModelViewSet):
|
||||
queryset = SupermarketCategoryRelation.objects
|
||||
serializer_class = SupermarketCategoryRelationSerializer
|
||||
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
|
||||
@@ -582,7 +622,7 @@ class SupermarketCategoryRelationViewSet(StandardFilterModelViewSet):
|
||||
return super().get_queryset()
|
||||
|
||||
|
||||
class KeywordViewSet(TreeMixin):
|
||||
class KeywordViewSet(LoggingMixin, TreeMixin):
|
||||
queryset = Keyword.objects
|
||||
model = Keyword
|
||||
serializer_class = KeywordSerializer
|
||||
@@ -590,7 +630,7 @@ class KeywordViewSet(TreeMixin):
|
||||
pagination_class = DefaultPagination
|
||||
|
||||
|
||||
class UnitViewSet(MergeMixin, FuzzyFilterMixin):
|
||||
class UnitViewSet(LoggingMixin, MergeMixin, FuzzyFilterMixin):
|
||||
queryset = Unit.objects
|
||||
model = Unit
|
||||
serializer_class = UnitSerializer
|
||||
@@ -598,7 +638,7 @@ class UnitViewSet(MergeMixin, FuzzyFilterMixin):
|
||||
pagination_class = DefaultPagination
|
||||
|
||||
|
||||
class FoodInheritFieldViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
class FoodInheritFieldViewSet(LoggingMixin, viewsets.ReadOnlyModelViewSet):
|
||||
queryset = FoodInheritField.objects
|
||||
serializer_class = FoodInheritFieldSerializer
|
||||
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
|
||||
@@ -610,7 +650,7 @@ class FoodInheritFieldViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
return super().get_queryset()
|
||||
|
||||
|
||||
class FoodViewSet(TreeMixin):
|
||||
class FoodViewSet(LoggingMixin, TreeMixin):
|
||||
queryset = Food.objects
|
||||
model = Food
|
||||
serializer_class = FoodSerializer
|
||||
@@ -761,7 +801,7 @@ class FoodViewSet(TreeMixin):
|
||||
OpenApiParameter(name='order_direction', description='Order ascending or descending', type=str,
|
||||
enum=['asc', 'desc']),
|
||||
]))
|
||||
class RecipeBookViewSet(StandardFilterModelViewSet):
|
||||
class RecipeBookViewSet(LoggingMixin, StandardFilterModelViewSet):
|
||||
queryset = RecipeBook.objects
|
||||
serializer_class = RecipeBookSerializer
|
||||
permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope]
|
||||
@@ -785,7 +825,7 @@ class RecipeBookViewSet(StandardFilterModelViewSet):
|
||||
OpenApiParameter(name='recipe', description='id of recipe - only return books for that recipe', type=int),
|
||||
OpenApiParameter(name='book', description='id of book - only return recipes in that book', type=int),
|
||||
]))
|
||||
class RecipeBookEntryViewSet(viewsets.ModelViewSet):
|
||||
class RecipeBookEntryViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
queryset = RecipeBookEntry.objects
|
||||
serializer_class = RecipeBookEntrySerializer
|
||||
permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope]
|
||||
@@ -820,7 +860,7 @@ MealPlanViewQueryParameters = [
|
||||
@extend_schema_view(list=extend_schema(parameters=MealPlanViewQueryParameters),
|
||||
ical=extend_schema(parameters=MealPlanViewQueryParameters,
|
||||
responses={(200, 'text/calendar'): OpenApiTypes.STR}))
|
||||
class MealPlanViewSet(viewsets.ModelViewSet):
|
||||
class MealPlanViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
queryset = MealPlan.objects
|
||||
serializer_class = MealPlanSerializer
|
||||
permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope]
|
||||
@@ -851,7 +891,7 @@ class MealPlanViewSet(viewsets.ModelViewSet):
|
||||
return meal_plans_to_ical(self.get_queryset(), f'meal_plan_{from_date}-{to_date}.ics')
|
||||
|
||||
|
||||
class AutoPlanViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet):
|
||||
class AutoPlanViewSet(LoggingMixin, mixins.CreateModelMixin, viewsets.GenericViewSet):
|
||||
serializer_class = AutoMealPlanSerializer
|
||||
http_method_names = ['post', 'options']
|
||||
|
||||
@@ -915,7 +955,7 @@ class AutoPlanViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet):
|
||||
return Response(serializer.errors, 400)
|
||||
|
||||
|
||||
class MealTypeViewSet(viewsets.ModelViewSet):
|
||||
class MealTypeViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
"""
|
||||
returns list of meal types created by the
|
||||
requesting user ordered by the order field.
|
||||
@@ -935,7 +975,7 @@ class MealTypeViewSet(viewsets.ModelViewSet):
|
||||
OpenApiParameter(name='food', description='ID of food to filter for', type=int),
|
||||
OpenApiParameter(name='unit', description='ID of unit to filter for', type=int),
|
||||
]))
|
||||
class IngredientViewSet(viewsets.ModelViewSet):
|
||||
class IngredientViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
queryset = Ingredient.objects
|
||||
serializer_class = IngredientSerializer
|
||||
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
|
||||
@@ -964,7 +1004,7 @@ class IngredientViewSet(viewsets.ModelViewSet):
|
||||
type=int, many=True),
|
||||
OpenApiParameter(name='query', description=_('Query string matched (fuzzy) against object name.'), type=str),
|
||||
]))
|
||||
class StepViewSet(viewsets.ModelViewSet):
|
||||
class StepViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
queryset = Step.objects
|
||||
serializer_class = StepSerializer
|
||||
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
|
||||
@@ -1082,7 +1122,7 @@ class RecipePagination(PageNumberPagination):
|
||||
description=_('Filter recipes that can be made with OnHand food. [''true''/''<b>false</b>'']'),
|
||||
type=bool),
|
||||
]))
|
||||
class RecipeViewSet(viewsets.ModelViewSet):
|
||||
class RecipeViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
queryset = Recipe.objects
|
||||
serializer_class = RecipeSerializer
|
||||
# TODO split read and write permission for meal plan guest
|
||||
@@ -1244,7 +1284,7 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
||||
|
||||
@extend_schema_view(list=extend_schema(
|
||||
parameters=[OpenApiParameter(name='food_id', description='ID of food to filter for', type=int), ]))
|
||||
class UnitConversionViewSet(viewsets.ModelViewSet):
|
||||
class UnitConversionViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
queryset = UnitConversion.objects
|
||||
serializer_class = UnitConversionSerializer
|
||||
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
|
||||
@@ -1267,7 +1307,7 @@ class UnitConversionViewSet(viewsets.ModelViewSet):
|
||||
enum=[m[0] for m in PropertyType.CHOICES])
|
||||
]
|
||||
))
|
||||
class PropertyTypeViewSet(viewsets.ModelViewSet):
|
||||
class PropertyTypeViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
queryset = PropertyType.objects
|
||||
serializer_class = PropertyTypeSerializer
|
||||
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
|
||||
@@ -1281,7 +1321,7 @@ class PropertyTypeViewSet(viewsets.ModelViewSet):
|
||||
return self.queryset.filter(space=self.request.space)
|
||||
|
||||
|
||||
class PropertyViewSet(viewsets.ModelViewSet):
|
||||
class PropertyViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
queryset = Property.objects
|
||||
serializer_class = PropertySerializer
|
||||
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
|
||||
@@ -1291,7 +1331,7 @@ class PropertyViewSet(viewsets.ModelViewSet):
|
||||
return self.queryset.filter(space=self.request.space)
|
||||
|
||||
|
||||
class ShoppingListRecipeViewSet(viewsets.ModelViewSet):
|
||||
class ShoppingListRecipeViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
queryset = ShoppingListRecipe.objects
|
||||
serializer_class = ShoppingListRecipeSerializer
|
||||
permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope]
|
||||
@@ -1317,7 +1357,7 @@ class ShoppingListRecipeViewSet(viewsets.ModelViewSet):
|
||||
description=_('Returns the shopping list entries sorted by supermarket category order.'),
|
||||
type=int),
|
||||
]))
|
||||
class ShoppingListEntryViewSet(viewsets.ModelViewSet):
|
||||
class ShoppingListEntryViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
queryset = ShoppingListEntry.objects
|
||||
serializer_class = ShoppingListEntrySerializer
|
||||
permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope]
|
||||
@@ -1395,7 +1435,7 @@ class ShoppingListEntryViewSet(viewsets.ModelViewSet):
|
||||
return Response(serializer.errors, 400)
|
||||
|
||||
|
||||
class ViewLogViewSet(viewsets.ModelViewSet):
|
||||
class ViewLogViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
queryset = ViewLog.objects
|
||||
serializer_class = ViewLogSerializer
|
||||
permission_classes = [CustomIsOwner & CustomTokenHasReadWriteScope]
|
||||
@@ -1408,7 +1448,7 @@ class ViewLogViewSet(viewsets.ModelViewSet):
|
||||
|
||||
@extend_schema_view(list=extend_schema(
|
||||
parameters=[OpenApiParameter(name='recipe', description='Filter for entries with the given recipe', type=int), ]))
|
||||
class CookLogViewSet(viewsets.ModelViewSet):
|
||||
class CookLogViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
queryset = CookLog.objects
|
||||
serializer_class = CookLogSerializer
|
||||
permission_classes = [CustomIsOwner & CustomTokenHasReadWriteScope]
|
||||
@@ -1420,7 +1460,7 @@ class CookLogViewSet(viewsets.ModelViewSet):
|
||||
return self.queryset.filter(space=self.request.space)
|
||||
|
||||
|
||||
class ImportLogViewSet(viewsets.ModelViewSet):
|
||||
class ImportLogViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
queryset = ImportLog.objects
|
||||
serializer_class = ImportLogSerializer
|
||||
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
|
||||
@@ -1430,7 +1470,7 @@ class ImportLogViewSet(viewsets.ModelViewSet):
|
||||
return self.queryset.filter(space=self.request.space)
|
||||
|
||||
|
||||
class ExportLogViewSet(viewsets.ModelViewSet):
|
||||
class ExportLogViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
queryset = ExportLog.objects
|
||||
serializer_class = ExportLogSerializer
|
||||
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
|
||||
@@ -1440,7 +1480,7 @@ class ExportLogViewSet(viewsets.ModelViewSet):
|
||||
return self.queryset.filter(space=self.request.space)
|
||||
|
||||
|
||||
class BookmarkletImportViewSet(viewsets.ModelViewSet):
|
||||
class BookmarkletImportViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
queryset = BookmarkletImport.objects
|
||||
serializer_class = BookmarkletImportSerializer
|
||||
permission_classes = [CustomIsUser & CustomTokenHasScope]
|
||||
@@ -1456,7 +1496,7 @@ class BookmarkletImportViewSet(viewsets.ModelViewSet):
|
||||
return self.queryset.filter(space=self.request.space).all()
|
||||
|
||||
|
||||
class UserFileViewSet(StandardFilterModelViewSet):
|
||||
class UserFileViewSet(LoggingMixin, StandardFilterModelViewSet):
|
||||
queryset = UserFile.objects
|
||||
serializer_class = UserFileSerializer
|
||||
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
|
||||
@@ -1468,7 +1508,7 @@ class UserFileViewSet(StandardFilterModelViewSet):
|
||||
return super().get_queryset()
|
||||
|
||||
|
||||
class AutomationViewSet(StandardFilterModelViewSet):
|
||||
class AutomationViewSet(LoggingMixin, StandardFilterModelViewSet):
|
||||
queryset = Automation.objects
|
||||
serializer_class = AutomationSerializer
|
||||
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
|
||||
@@ -1497,7 +1537,7 @@ class AutomationViewSet(StandardFilterModelViewSet):
|
||||
@extend_schema_view(list=extend_schema(parameters=[
|
||||
OpenApiParameter(name='internal_note', description=_('I have no idea what internal_note is for.'), type=str)
|
||||
]))
|
||||
class InviteLinkViewSet(StandardFilterModelViewSet):
|
||||
class InviteLinkViewSet(LoggingMixin, StandardFilterModelViewSet):
|
||||
queryset = InviteLink.objects
|
||||
serializer_class = InviteLinkSerializer
|
||||
permission_classes = [CustomIsSpaceOwner & CustomIsAdmin & CustomTokenHasReadWriteScope]
|
||||
@@ -1524,7 +1564,7 @@ class InviteLinkViewSet(StandardFilterModelViewSet):
|
||||
enum=[m[0] for m in CustomFilter.MODELS])
|
||||
]
|
||||
))
|
||||
class CustomFilterViewSet(StandardFilterModelViewSet):
|
||||
class CustomFilterViewSet(LoggingMixin, StandardFilterModelViewSet):
|
||||
queryset = CustomFilter.objects
|
||||
serializer_class = CustomFilterSerializer
|
||||
permission_classes = [CustomIsOwner & CustomTokenHasReadWriteScope]
|
||||
@@ -1540,7 +1580,7 @@ class CustomFilterViewSet(StandardFilterModelViewSet):
|
||||
return super().get_queryset()
|
||||
|
||||
|
||||
class AccessTokenViewSet(viewsets.ModelViewSet):
|
||||
class AccessTokenViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||
queryset = AccessToken.objects
|
||||
serializer_class = AccessTokenSerializer
|
||||
permission_classes = [CustomIsOwner & CustomTokenHasReadWriteScope]
|
||||
|
||||
@@ -31,6 +31,7 @@ from cookbook.integration.recipesage import RecipeSage
|
||||
from cookbook.integration.rezeptsuitede import Rezeptsuitede
|
||||
from cookbook.integration.rezkonv import RezKonv
|
||||
from cookbook.integration.saffron import Saffron
|
||||
from cookbook.integration.gourmet import Gourmet
|
||||
from cookbook.models import ExportLog, Recipe
|
||||
from recipes import settings
|
||||
|
||||
@@ -80,6 +81,8 @@ def get_integration(request, export_type):
|
||||
return Cookmate(request, export_type)
|
||||
if export_type == ImportExportBase.REZEPTSUITEDE:
|
||||
return Rezeptsuitede(request, export_type)
|
||||
if export_type == ImportExportBase.GOURMET:
|
||||
return Gourmet(request, export_type)
|
||||
|
||||
|
||||
@group_required('user')
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from io import StringIO
|
||||
from uuid import UUID
|
||||
|
||||
import redis
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
@@ -17,6 +18,7 @@ from django.http import HttpResponseRedirect, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils import timezone
|
||||
from django.utils.datetime_safe import date
|
||||
from django.utils.translation import gettext as _
|
||||
from django_scopes import scopes_disabled
|
||||
from drf_spectacular.views import SpectacularRedocView, SpectacularSwaggerView
|
||||
@@ -354,6 +356,43 @@ def system(request):
|
||||
for key in migration_info.keys():
|
||||
migration_info[key]['total'] = len(migration_info[key]['unapplied_migrations']) + len(migration_info[key]['applied_migrations'])
|
||||
|
||||
# API endpoint logging
|
||||
r = redis.StrictRedis(
|
||||
host=settings.REDIS_HOST,
|
||||
port=settings.REDIS_PORT,
|
||||
password='',
|
||||
username='',
|
||||
db=settings.REDIS_DATABASES['STATS'],
|
||||
)
|
||||
|
||||
api_stats = [['Endpoint', 'Total']]
|
||||
api_space_stats = [['User', 'Total']]
|
||||
total_stats = ['All', int(r.get('api:request-count'))]
|
||||
|
||||
for i in range(0, 6):
|
||||
d = (date.today() - timedelta(days=i)).isoformat()
|
||||
api_stats[0].append(d)
|
||||
api_space_stats[0].append(d)
|
||||
total_stats.append(int(r.get(f'api:request-count:{d}')) if r.get(f'api:request-count:{d}') else 0)
|
||||
|
||||
api_stats.append(total_stats)
|
||||
|
||||
for x in r.zrange('api:endpoint-request-count', 0, -1, withscores=True, desc=True):
|
||||
endpoint = x[0].decode('utf-8')
|
||||
endpoint_stats = [endpoint, x[1]]
|
||||
for i in range(0, 6):
|
||||
d = (date.today() - timedelta(days=i)).isoformat()
|
||||
endpoint_stats.append(r.zscore(f'api:endpoint-request-count:{d}', endpoint))
|
||||
api_stats.append(endpoint_stats)
|
||||
|
||||
for x in r.zrange('api:space-request-count', 0, 20, withscores=True, desc=True):
|
||||
s = x[0].decode('utf-8')
|
||||
space_stats = [Space.objects.get(pk=s).name, x[1]]
|
||||
for i in range(0, 6):
|
||||
d = (date.today() - timedelta(days=i)).isoformat()
|
||||
space_stats.append(r.zscore(f'api:space-request-count:{d}', s))
|
||||
api_space_stats.append(space_stats)
|
||||
|
||||
return render(
|
||||
request, 'system.html', {
|
||||
'gunicorn_media': settings.GUNICORN_MEDIA,
|
||||
|
||||
Reference in New Issue
Block a user