api stats

This commit is contained in:
vabene1111
2024-11-12 15:53:13 +01:00
parent 1777dfe821
commit 0dea5c9877
5 changed files with 172 additions and 55 deletions

View File

@@ -82,9 +82,9 @@
{% else %} {% else %}
{% trans 'Everything is fine!' %} {% trans 'Everything is fine!' %}
{% endif %} {% endif %}
<h4 class="mt-3">{% trans 'Allowed Hosts' %} <span <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> {% trans 'Warning' %}{% else %}{% trans 'Ok' %}{% endif %}</span></h4>
{% if debug %} {% if debug %}
{% blocktrans %} {% blocktrans %}
@@ -176,6 +176,33 @@
{#{% endfor %}#} {#{% endfor %}#}
{# </textarea>#} {# </textarea>#}
<h4 class="mt-3">API Stats</h4>
<h6 >User Stats</h6>
<table class="table table-bordered table-striped">
{% for r in api_user_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> <h4 class="mt-3">Debug</h4>
<textarea class="form-control" rows="20"> <textarea class="form-control" rows="20">
Gunicorn Media: {{ gunicorn_media }} Gunicorn Media: {{ gunicorn_media }}

View File

@@ -12,6 +12,7 @@ from json import JSONDecodeError
from urllib.parse import unquote from urllib.parse import unquote
from zipfile import ZipFile from zipfile import ZipFile
import redis
import requests import requests
from PIL import UnidentifiedImageError from PIL import UnidentifiedImageError
from annoying.decorators import ajax_request from annoying.decorators import ajax_request
@@ -30,6 +31,7 @@ from django.http import FileResponse, HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.datetime_safe import date
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
from icalendar import Calendar, Event from icalendar import Calendar, Event
@@ -101,6 +103,46 @@ from recipes import settings
from recipes.settings import DRF_THROTTLE_RECIPE_URL_IMPORT, FDC_API_KEY from recipes.settings import DRF_THROTTLE_RECIPE_URL_IMPORT, FDC_API_KEY
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()
user = request.user
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:user-request-count', 1, user.pk)
pipe.zincrby(f'api:user-request-count:{d}', 1, user.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()
class StandardFilterMixin(ViewSetMixin): class StandardFilterMixin(ViewSetMixin):
def get_queryset(self): def get_queryset(self):
@@ -184,9 +226,9 @@ class FuzzyFilterMixin(ViewSetMixin, ExtendedRecipeMixin):
if query is not None and query not in ["''", '']: if query is not None and query not in ["''", '']:
if fuzzy and (settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql'): if fuzzy and (settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql'):
if self.request.user.is_authenticated and any( if self.request.user.is_authenticated and any(
[self.model.__name__.lower() in x for x in self.request.user.searchpreference.unaccent.values_list('field', flat=True)] [self.model.__name__.lower() in x for x in self.request.user.searchpreference.unaccent.values_list('field', flat=True)]
): ):
self.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name__unaccent', query)) self.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name__unaccent', query))
else: else:
self.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name', query)) self.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name', query))
self.queryset = self.queryset.order_by('-trigram') self.queryset = self.queryset.order_by('-trigram')
@@ -370,7 +412,7 @@ class TreeMixin(MergeMixin, FuzzyFilterMixin, ExtendedRecipeMixin):
return Response(content, status=status.HTTP_400_BAD_REQUEST) return Response(content, status=status.HTTP_400_BAD_REQUEST)
class UserViewSet(viewsets.ModelViewSet): class UserViewSet(LoggingMixin, viewsets.ModelViewSet):
""" """
list: list:
optional parameters optional parameters
@@ -394,14 +436,14 @@ class UserViewSet(viewsets.ModelViewSet):
return queryset return queryset
class GroupViewSet(viewsets.ModelViewSet): class GroupViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = Group.objects.all() queryset = Group.objects.all()
serializer_class = GroupSerializer serializer_class = GroupSerializer
permission_classes = [CustomIsAdmin & CustomTokenHasReadWriteScope] permission_classes = [CustomIsAdmin & CustomTokenHasReadWriteScope]
http_method_names = ['get', ] http_method_names = ['get', ]
class SpaceViewSet(viewsets.ModelViewSet): class SpaceViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = Space.objects queryset = Space.objects
serializer_class = SpaceSerializer serializer_class = SpaceSerializer
permission_classes = [IsReadOnlyDRF & CustomIsGuest | CustomIsOwner & CustomIsAdmin & CustomTokenHasReadWriteScope] permission_classes = [IsReadOnlyDRF & CustomIsGuest | CustomIsOwner & CustomIsAdmin & CustomTokenHasReadWriteScope]
@@ -411,7 +453,7 @@ class SpaceViewSet(viewsets.ModelViewSet):
return self.queryset.filter(id=self.request.space.id) return self.queryset.filter(id=self.request.space.id)
class UserSpaceViewSet(viewsets.ModelViewSet): class UserSpaceViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = UserSpace.objects queryset = UserSpace.objects
serializer_class = UserSpaceSerializer serializer_class = UserSpaceSerializer
permission_classes = [(CustomIsSpaceOwner | CustomIsOwnerReadOnly) & CustomTokenHasReadWriteScope] permission_classes = [(CustomIsSpaceOwner | CustomIsOwnerReadOnly) & CustomTokenHasReadWriteScope]
@@ -434,7 +476,7 @@ class UserSpaceViewSet(viewsets.ModelViewSet):
return self.queryset.filter(user=self.request.user, space=self.request.space) return self.queryset.filter(user=self.request.user, space=self.request.space)
class UserPreferenceViewSet(viewsets.ModelViewSet): class UserPreferenceViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = UserPreference.objects queryset = UserPreference.objects
serializer_class = UserPreferenceSerializer serializer_class = UserPreferenceSerializer
permission_classes = [CustomIsOwner & CustomTokenHasReadWriteScope] permission_classes = [CustomIsOwner & CustomTokenHasReadWriteScope]
@@ -445,7 +487,7 @@ class UserPreferenceViewSet(viewsets.ModelViewSet):
return self.queryset.filter(user=self.request.user) 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 # TODO handle delete protect error and adjust test
queryset = Storage.objects queryset = Storage.objects
serializer_class = StorageSerializer serializer_class = StorageSerializer
@@ -455,7 +497,7 @@ class StorageViewSet(viewsets.ModelViewSet):
return self.queryset.filter(space=self.request.space) return self.queryset.filter(space=self.request.space)
class ConnectorConfigConfigViewSet(viewsets.ModelViewSet): class ConnectorConfigConfigViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = ConnectorConfig.objects queryset = ConnectorConfig.objects
serializer_class = ConnectorConfigConfigSerializer serializer_class = ConnectorConfigConfigSerializer
permission_classes = [CustomIsAdmin & CustomTokenHasReadWriteScope] permission_classes = [CustomIsAdmin & CustomTokenHasReadWriteScope]
@@ -464,7 +506,7 @@ class ConnectorConfigConfigViewSet(viewsets.ModelViewSet):
return self.queryset.filter(space=self.request.space) return self.queryset.filter(space=self.request.space)
class SyncViewSet(viewsets.ModelViewSet): class SyncViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = Sync.objects queryset = Sync.objects
serializer_class = SyncSerializer serializer_class = SyncSerializer
permission_classes = [CustomIsAdmin & CustomTokenHasReadWriteScope] permission_classes = [CustomIsAdmin & CustomTokenHasReadWriteScope]
@@ -473,7 +515,7 @@ class SyncViewSet(viewsets.ModelViewSet):
return self.queryset.filter(space=self.request.space) return self.queryset.filter(space=self.request.space)
class SyncLogViewSet(viewsets.ReadOnlyModelViewSet): class SyncLogViewSet(LoggingMixin, viewsets.ReadOnlyModelViewSet):
queryset = SyncLog.objects queryset = SyncLog.objects
serializer_class = SyncLogSerializer serializer_class = SyncLogSerializer
permission_classes = [CustomIsAdmin & CustomTokenHasReadWriteScope] permission_classes = [CustomIsAdmin & CustomTokenHasReadWriteScope]
@@ -483,7 +525,7 @@ class SyncLogViewSet(viewsets.ReadOnlyModelViewSet):
return self.queryset.filter(sync__space=self.request.space) return self.queryset.filter(sync__space=self.request.space)
class SupermarketViewSet(viewsets.ModelViewSet, StandardFilterMixin): class SupermarketViewSet(LoggingMixin, viewsets.ModelViewSet, StandardFilterMixin):
schema = FilterSchema() schema = FilterSchema()
queryset = Supermarket.objects queryset = Supermarket.objects
serializer_class = SupermarketSerializer serializer_class = SupermarketSerializer
@@ -494,7 +536,7 @@ class SupermarketViewSet(viewsets.ModelViewSet, StandardFilterMixin):
return super().get_queryset() return super().get_queryset()
class SupermarketCategoryViewSet(viewsets.ModelViewSet, FuzzyFilterMixin, MergeMixin): class SupermarketCategoryViewSet(LoggingMixin, viewsets.ModelViewSet, FuzzyFilterMixin, MergeMixin):
queryset = SupermarketCategory.objects queryset = SupermarketCategory.objects
model = SupermarketCategory model = SupermarketCategory
serializer_class = SupermarketCategorySerializer serializer_class = SupermarketCategorySerializer
@@ -505,7 +547,7 @@ class SupermarketCategoryViewSet(viewsets.ModelViewSet, FuzzyFilterMixin, MergeM
return super().get_queryset() return super().get_queryset()
class SupermarketCategoryRelationViewSet(viewsets.ModelViewSet, StandardFilterMixin): class SupermarketCategoryRelationViewSet(LoggingMixin, viewsets.ModelViewSet, StandardFilterMixin):
queryset = SupermarketCategoryRelation.objects queryset = SupermarketCategoryRelation.objects
serializer_class = SupermarketCategoryRelationSerializer serializer_class = SupermarketCategoryRelationSerializer
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope] permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
@@ -516,7 +558,7 @@ class SupermarketCategoryRelationViewSet(viewsets.ModelViewSet, StandardFilterMi
return super().get_queryset() return super().get_queryset()
class KeywordViewSet(viewsets.ModelViewSet, TreeMixin): class KeywordViewSet(LoggingMixin, viewsets.ModelViewSet, TreeMixin):
queryset = Keyword.objects queryset = Keyword.objects
model = Keyword model = Keyword
serializer_class = KeywordSerializer serializer_class = KeywordSerializer
@@ -524,7 +566,7 @@ class KeywordViewSet(viewsets.ModelViewSet, TreeMixin):
pagination_class = DefaultPagination pagination_class = DefaultPagination
class UnitViewSet(viewsets.ModelViewSet, MergeMixin, FuzzyFilterMixin): class UnitViewSet(LoggingMixin, viewsets.ModelViewSet, MergeMixin, FuzzyFilterMixin):
queryset = Unit.objects queryset = Unit.objects
model = Unit model = Unit
serializer_class = UnitSerializer serializer_class = UnitSerializer
@@ -532,7 +574,7 @@ class UnitViewSet(viewsets.ModelViewSet, MergeMixin, FuzzyFilterMixin):
pagination_class = DefaultPagination pagination_class = DefaultPagination
class FoodInheritFieldViewSet(viewsets.ReadOnlyModelViewSet): class FoodInheritFieldViewSet(LoggingMixin, viewsets.ReadOnlyModelViewSet):
queryset = FoodInheritField.objects queryset = FoodInheritField.objects
serializer_class = FoodInheritFieldSerializer serializer_class = FoodInheritFieldSerializer
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope] permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
@@ -543,7 +585,7 @@ class FoodInheritFieldViewSet(viewsets.ReadOnlyModelViewSet):
return super().get_queryset() return super().get_queryset()
class FoodViewSet(viewsets.ModelViewSet, TreeMixin): class FoodViewSet(LoggingMixin, viewsets.ModelViewSet, TreeMixin):
queryset = Food.objects queryset = Food.objects
model = Food model = Food
serializer_class = FoodSerializer serializer_class = FoodSerializer
@@ -610,8 +652,8 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
return JsonResponse( return JsonResponse(
{ {
'msg': 'msg':
'API Key Rate Limit reached/exceeded, see https://api.data.gov/docs/rate-limits/ for more information. \ 'API Key Rate Limit reached/exceeded, see https://api.data.gov/docs/rate-limits/ for more information. \
Configure your key in Tandoor using environment FDC_API_KEY variable.' Configure your key in Tandoor using environment FDC_API_KEY variable.'
}, },
status=429, status=429,
json_dumps_params={'indent': 4}) json_dumps_params={'indent': 4})
@@ -680,7 +722,7 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
return Response(content, status=status.HTTP_403_FORBIDDEN) return Response(content, status=status.HTTP_403_FORBIDDEN)
class RecipeBookViewSet(viewsets.ModelViewSet, StandardFilterMixin): class RecipeBookViewSet(LoggingMixin, viewsets.ModelViewSet, StandardFilterMixin):
queryset = RecipeBook.objects queryset = RecipeBook.objects
serializer_class = RecipeBookSerializer serializer_class = RecipeBookSerializer
permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope] permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope]
@@ -698,7 +740,7 @@ class RecipeBookViewSet(viewsets.ModelViewSet, StandardFilterMixin):
return super().get_queryset() return super().get_queryset()
class RecipeBookEntryViewSet(viewsets.ModelViewSet, viewsets.GenericViewSet): class RecipeBookEntryViewSet(LoggingMixin, viewsets.ModelViewSet, viewsets.GenericViewSet):
""" """
list: list:
optional parameters optional parameters
@@ -724,7 +766,7 @@ class RecipeBookEntryViewSet(viewsets.ModelViewSet, viewsets.GenericViewSet):
return queryset return queryset
class MealPlanViewSet(viewsets.ModelViewSet): class MealPlanViewSet(LoggingMixin, viewsets.ModelViewSet):
""" """
list: list:
optional parameters optional parameters
@@ -762,7 +804,7 @@ class MealPlanViewSet(viewsets.ModelViewSet):
return queryset return queryset
class AutoPlanViewSet(viewsets.ViewSet): class AutoPlanViewSet(LoggingMixin, viewsets.ViewSet):
def create(self, request): def create(self, request):
serializer = AutoMealPlanSerializer(data=request.data) serializer = AutoMealPlanSerializer(data=request.data)
@@ -824,7 +866,7 @@ class AutoPlanViewSet(viewsets.ViewSet):
return Response(serializer.errors, 400) return Response(serializer.errors, 400)
class MealTypeViewSet(viewsets.ModelViewSet): class MealTypeViewSet(LoggingMixin, viewsets.ModelViewSet):
""" """
returns list of meal types created by the returns list of meal types created by the
requesting user ordered by the order field. requesting user ordered by the order field.
@@ -838,7 +880,7 @@ class MealTypeViewSet(viewsets.ModelViewSet):
return queryset return queryset
class IngredientViewSet(viewsets.ModelViewSet): class IngredientViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = Ingredient.objects queryset = Ingredient.objects
serializer_class = IngredientSerializer serializer_class = IngredientSerializer
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope] permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
@@ -862,7 +904,7 @@ class IngredientViewSet(viewsets.ModelViewSet):
return queryset.select_related('food') return queryset.select_related('food')
class StepViewSet(viewsets.ModelViewSet): class StepViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = Step.objects queryset = Step.objects
serializer_class = StepSerializer serializer_class = StepSerializer
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope] permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
@@ -897,7 +939,7 @@ class RecipePagination(PageNumberPagination):
return Response(OrderedDict([('count', self.page.paginator.count), ('next', self.get_next_link()), ('previous', self.get_previous_link()), ('results', data), ])) return Response(OrderedDict([('count', self.page.paginator.count), ('next', self.get_next_link()), ('previous', self.get_previous_link()), ('results', data), ]))
class RecipeViewSet(viewsets.ModelViewSet): class RecipeViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = Recipe.objects queryset = Recipe.objects
serializer_class = RecipeSerializer serializer_class = RecipeSerializer
# TODO split read and write permission for meal plan guest # TODO split read and write permission for meal plan guest
@@ -1064,7 +1106,7 @@ class RecipeViewSet(viewsets.ModelViewSet):
return Response(self.serializer_class(qs, many=True).data) return Response(self.serializer_class(qs, many=True).data)
class UnitConversionViewSet(viewsets.ModelViewSet): class UnitConversionViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = UnitConversion.objects queryset = UnitConversion.objects
serializer_class = UnitConversionSerializer serializer_class = UnitConversionSerializer
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope] permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
@@ -1081,7 +1123,7 @@ class UnitConversionViewSet(viewsets.ModelViewSet):
return self.queryset.filter(space=self.request.space) return self.queryset.filter(space=self.request.space)
class PropertyTypeViewSet(viewsets.ModelViewSet): class PropertyTypeViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = PropertyType.objects queryset = PropertyType.objects
serializer_class = PropertyTypeSerializer serializer_class = PropertyTypeSerializer
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope] permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
@@ -1090,7 +1132,7 @@ class PropertyTypeViewSet(viewsets.ModelViewSet):
return self.queryset.filter(space=self.request.space) return self.queryset.filter(space=self.request.space)
class PropertyViewSet(viewsets.ModelViewSet): class PropertyViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = Property.objects queryset = Property.objects
serializer_class = PropertySerializer serializer_class = PropertySerializer
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope] permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
@@ -1099,7 +1141,7 @@ class PropertyViewSet(viewsets.ModelViewSet):
return self.queryset.filter(space=self.request.space) return self.queryset.filter(space=self.request.space)
class ShoppingListRecipeViewSet(viewsets.ModelViewSet): class ShoppingListRecipeViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = ShoppingListRecipe.objects queryset = ShoppingListRecipe.objects
serializer_class = ShoppingListRecipeSerializer serializer_class = ShoppingListRecipeSerializer
permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope] permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope]
@@ -1110,10 +1152,10 @@ class ShoppingListRecipeViewSet(viewsets.ModelViewSet):
Q(entries__isnull=True) Q(entries__isnull=True)
| Q(entries__created_by=self.request.user) | Q(entries__created_by=self.request.user)
| Q(entries__created_by__in=list(self.request.user.get_shopping_share())) | Q(entries__created_by__in=list(self.request.user.get_shopping_share()))
).distinct().all() ).distinct().all()
class ShoppingListEntryViewSet(viewsets.ModelViewSet): class ShoppingListEntryViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = ShoppingListEntry.objects queryset = ShoppingListEntry.objects
serializer_class = ShoppingListEntrySerializer serializer_class = ShoppingListEntrySerializer
permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope] permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope]
@@ -1123,7 +1165,7 @@ class ShoppingListEntryViewSet(viewsets.ModelViewSet):
name='checked', name='checked',
description=_('Filter shopping list entries on checked. [''true'', ''false'', ''both'', ''<b>recent</b>'']<br> \ description=_('Filter shopping list entries on checked. [''true'', ''false'', ''both'', ''<b>recent</b>'']<br> \
- ''recent'' includes unchecked items and recently completed items.') - ''recent'' includes unchecked items and recently completed items.')
), ),
QueryParam(name='supermarket', description=_('Returns the shopping list entries sorted by supermarket category order.'), qtype='integer'), QueryParam(name='supermarket', description=_('Returns the shopping list entries sorted by supermarket category order.'), qtype='integer'),
] ]
schema = QueryParamAutoSchema() schema = QueryParamAutoSchema()
@@ -1171,7 +1213,7 @@ class ShoppingListEntryViewSet(viewsets.ModelViewSet):
print(serializer.validated_data) print(serializer.validated_data)
bulk_entries = ShoppingListEntry.objects.filter( bulk_entries = ShoppingListEntry.objects.filter(
Q(created_by=self.request.user) | Q(created_by__in=list(self.request.user.get_shopping_share())) Q(created_by=self.request.user) | Q(created_by__in=list(self.request.user.get_shopping_share()))
).filter(space=request.space, id__in=serializer.validated_data['ids']) ).filter(space=request.space, id__in=serializer.validated_data['ids'])
bulk_entries.update(checked=(checked := serializer.validated_data['checked']), updated_at=timezone.now(), ) bulk_entries.update(checked=(checked := serializer.validated_data['checked']), updated_at=timezone.now(), )
# update the onhand for food if shopping_add_onhand is True # update the onhand for food if shopping_add_onhand is True
@@ -1189,7 +1231,7 @@ class ShoppingListEntryViewSet(viewsets.ModelViewSet):
return Response(serializer.errors, 400) return Response(serializer.errors, 400)
class ViewLogViewSet(viewsets.ModelViewSet): class ViewLogViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = ViewLog.objects queryset = ViewLog.objects
serializer_class = ViewLogSerializer serializer_class = ViewLogSerializer
permission_classes = [CustomIsOwner & CustomTokenHasReadWriteScope] permission_classes = [CustomIsOwner & CustomTokenHasReadWriteScope]
@@ -1200,7 +1242,7 @@ class ViewLogViewSet(viewsets.ModelViewSet):
return self.queryset.filter(created_by=self.request.user).filter(space=self.request.space) return self.queryset.filter(created_by=self.request.user).filter(space=self.request.space)
class CookLogViewSet(viewsets.ModelViewSet): class CookLogViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = CookLog.objects queryset = CookLog.objects
serializer_class = CookLogSerializer serializer_class = CookLogSerializer
permission_classes = [CustomIsOwner & CustomTokenHasReadWriteScope] permission_classes = [CustomIsOwner & CustomTokenHasReadWriteScope]
@@ -1215,7 +1257,7 @@ class CookLogViewSet(viewsets.ModelViewSet):
return self.queryset.filter(space=self.request.space) return self.queryset.filter(space=self.request.space)
class ImportLogViewSet(viewsets.ModelViewSet): class ImportLogViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = ImportLog.objects queryset = ImportLog.objects
serializer_class = ImportLogSerializer serializer_class = ImportLogSerializer
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope] permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
@@ -1225,7 +1267,7 @@ class ImportLogViewSet(viewsets.ModelViewSet):
return self.queryset.filter(space=self.request.space) return self.queryset.filter(space=self.request.space)
class ExportLogViewSet(viewsets.ModelViewSet): class ExportLogViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = ExportLog.objects queryset = ExportLog.objects
serializer_class = ExportLogSerializer serializer_class = ExportLogSerializer
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope] permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
@@ -1235,7 +1277,7 @@ class ExportLogViewSet(viewsets.ModelViewSet):
return self.queryset.filter(space=self.request.space) return self.queryset.filter(space=self.request.space)
class BookmarkletImportViewSet(viewsets.ModelViewSet): class BookmarkletImportViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = BookmarkletImport.objects queryset = BookmarkletImport.objects
serializer_class = BookmarkletImportSerializer serializer_class = BookmarkletImportSerializer
permission_classes = [CustomIsUser & CustomTokenHasScope] permission_classes = [CustomIsUser & CustomTokenHasScope]
@@ -1250,7 +1292,7 @@ class BookmarkletImportViewSet(viewsets.ModelViewSet):
return self.queryset.filter(space=self.request.space).all() return self.queryset.filter(space=self.request.space).all()
class UserFileViewSet(viewsets.ModelViewSet, StandardFilterMixin): class UserFileViewSet(LoggingMixin, viewsets.ModelViewSet, StandardFilterMixin):
schema = FilterSchema() schema = FilterSchema()
queryset = UserFile.objects queryset = UserFile.objects
serializer_class = UserFileSerializer serializer_class = UserFileSerializer
@@ -1262,7 +1304,7 @@ class UserFileViewSet(viewsets.ModelViewSet, StandardFilterMixin):
return super().get_queryset() return super().get_queryset()
class AutomationViewSet(viewsets.ModelViewSet, StandardFilterMixin): class AutomationViewSet(LoggingMixin, viewsets.ModelViewSet, StandardFilterMixin):
""" """
list: list:
optional parameters optional parameters
@@ -1313,7 +1355,7 @@ class AutomationViewSet(viewsets.ModelViewSet, StandardFilterMixin):
return super().get_queryset() return super().get_queryset()
class InviteLinkViewSet(viewsets.ModelViewSet, StandardFilterMixin): class InviteLinkViewSet(LoggingMixin, viewsets.ModelViewSet, StandardFilterMixin):
queryset = InviteLink.objects queryset = InviteLink.objects
serializer_class = InviteLinkSerializer serializer_class = InviteLinkSerializer
permission_classes = [CustomIsSpaceOwner & CustomIsAdmin & CustomTokenHasReadWriteScope] permission_classes = [CustomIsSpaceOwner & CustomIsAdmin & CustomTokenHasReadWriteScope]
@@ -1331,7 +1373,7 @@ class InviteLinkViewSet(viewsets.ModelViewSet, StandardFilterMixin):
return None return None
class CustomFilterViewSet(viewsets.ModelViewSet, StandardFilterMixin): class CustomFilterViewSet(LoggingMixin, viewsets.ModelViewSet, StandardFilterMixin):
queryset = CustomFilter.objects queryset = CustomFilter.objects
serializer_class = CustomFilterSerializer serializer_class = CustomFilterSerializer
permission_classes = [CustomIsOwner & CustomTokenHasReadWriteScope] permission_classes = [CustomIsOwner & CustomTokenHasReadWriteScope]
@@ -1342,7 +1384,7 @@ class CustomFilterViewSet(viewsets.ModelViewSet, StandardFilterMixin):
return super().get_queryset() return super().get_queryset()
class AccessTokenViewSet(viewsets.ModelViewSet): class AccessTokenViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = AccessToken.objects queryset = AccessToken.objects
serializer_class = AccessTokenSerializer serializer_class = AccessTokenSerializer
permission_classes = [CustomIsOwner & CustomTokenHasReadWriteScope] permission_classes = [CustomIsOwner & CustomTokenHasReadWriteScope]
@@ -1467,7 +1509,7 @@ class RecipeUrlImportView(APIView):
'recipe_json': helper.get_from_scraper(scrape, request), 'recipe_json': helper.get_from_scraper(scrape, request),
'recipe_images': list(dict.fromkeys(get_images_from_soup(scrape.soup, url))), 'recipe_images': list(dict.fromkeys(get_images_from_soup(scrape.soup, url))),
}, },
status=status.HTTP_200_OK) status=status.HTTP_200_OK)
else: else:
return Response({'error': True, 'msg': _('No usable data could be found.')}, status=status.HTTP_400_BAD_REQUEST) return Response({'error': True, 'msg': _('No usable data could be found.')}, status=status.HTTP_400_BAD_REQUEST)
@@ -1678,7 +1720,7 @@ def sync_all(request):
# @schema(AutoSchema()) #TODO add proper schema # @schema(AutoSchema()) #TODO add proper schema
@permission_classes([CustomIsUser & CustomTokenHasReadWriteScope]) @permission_classes([CustomIsUser & CustomTokenHasReadWriteScope])
def share_link(request, pk): def share_link(request, pk):
if request.space.allow_sharing and has_group_permission(request.user, ('user', )): if request.space.allow_sharing and has_group_permission(request.user, ('user',)):
recipe = get_object_or_404(Recipe, pk=pk, space=request.space) recipe = get_object_or_404(Recipe, pk=pk, space=request.space)
link = ShareLink.objects.create(recipe=recipe, created_by=request.user, space=request.space) link = ShareLink.objects.create(recipe=recipe, created_by=request.user, space=request.space)
return JsonResponse({'pk': pk, 'share': link.uuid, 'link': request.build_absolute_uri(reverse('view_recipe', args=[pk, link.uuid]))}) return JsonResponse({'pk': pk, 'share': link.uuid, 'link': request.build_absolute_uri(reverse('view_recipe', args=[pk, link.uuid]))})

View File

@@ -1,9 +1,10 @@
import os import os
import re import re
from datetime import datetime from datetime import datetime, timedelta
from io import StringIO from io import StringIO
from uuid import UUID from uuid import UUID
import redis
from django.apps import apps from django.apps import apps
from django.conf import settings from django.conf import settings
from django.contrib import messages 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.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils import timezone from django.utils import timezone
from django.utils.datetime_safe import date
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
@@ -38,7 +40,8 @@ def index(request):
return HttpResponseRedirect(reverse_lazy('view_search')) return HttpResponseRedirect(reverse_lazy('view_search'))
try: try:
page_map = {UserPreference.SEARCH: reverse_lazy('view_search'), UserPreference.PLAN: reverse_lazy('view_plan'), UserPreference.BOOKS: reverse_lazy('view_books'), UserPreference.SHOPPING: reverse_lazy('view_shopping'),} page_map = {UserPreference.SEARCH: reverse_lazy('view_search'), UserPreference.PLAN: reverse_lazy('view_plan'), UserPreference.BOOKS: reverse_lazy('view_books'),
UserPreference.SHOPPING: reverse_lazy('view_shopping'), }
return HttpResponseRedirect(page_map.get(request.user.userpreference.default_page)) return HttpResponseRedirect(page_map.get(request.user.userpreference.default_page))
except UserPreference.DoesNotExist: except UserPreference.DoesNotExist:
@@ -160,7 +163,6 @@ def recipe_view(request, pk, share=None):
return render(request, 'recipe_view.html', {'recipe': recipe, 'comments': comments, 'comment_form': comment_form, 'share': share, 'servings': servings}) return render(request, 'recipe_view.html', {'recipe': recipe, 'comments': comments, 'comment_form': comment_form, 'share': share, 'servings': servings})
@group_required('user') @group_required('user')
def books(request): def books(request):
return render(request, 'books.html', {}) return render(request, 'books.html', {})
@@ -344,11 +346,48 @@ def system(request):
for key in migration_info.keys(): for key in migration_info.keys():
migration_info[key]['total'] = len(migration_info[key]['unapplied_migrations']) + len(migration_info[key]['applied_migrations']) 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_user_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_user_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:user-request-count', 0, 20, withscores=True, desc=True):
u = x[0].decode('utf-8')
user_stats = [User.objects.get(pk=u).username, x[1]]
for i in range(0, 6):
d = (date.today() - timedelta(days=i)).isoformat()
user_stats.append(r.zscore(f'api:user-request-count:{d}', u))
api_user_stats.append(user_stats)
return render( return render(
request, 'system.html', { request, 'system.html', {
'gunicorn_media': settings.GUNICORN_MEDIA, 'debug': settings.DEBUG, 'postgres': postgres, 'postgres_version': postgres_ver, 'postgres_status': database_status, 'gunicorn_media': settings.GUNICORN_MEDIA, 'debug': settings.DEBUG, 'postgres': postgres, 'postgres_version': postgres_ver, 'postgres_status': database_status,
'postgres_message': database_message, 'version_info': VERSION_INFO, 'plugins': PLUGINS, 'secret_key': secret_key, 'orphans': orphans, 'migration_info': migration_info, 'postgres_message': database_message, 'version_info': VERSION_INFO, 'plugins': PLUGINS, 'secret_key': secret_key, 'orphans': orphans, 'migration_info': migration_info,
'missing_migration': missing_migration, 'allowed_hosts': settings.ALLOWED_HOSTS, 'missing_migration': missing_migration, 'allowed_hosts': settings.ALLOWED_HOSTS, 'api_stats': api_stats, 'api_user_stats': api_user_stats
}) })

View File

@@ -126,6 +126,14 @@ PRIVACY_URL = os.getenv('PRIVACY_URL', '')
IMPRINT_URL = os.getenv('IMPRINT_URL', '') IMPRINT_URL = os.getenv('IMPRINT_URL', '')
HOSTED = bool(int(os.getenv('HOSTED', False))) HOSTED = bool(int(os.getenv('HOSTED', False)))
REDIS_HOST = os.getenv('REDIS_HOST', None)
REDIS_PORT = int(os.getenv('REDIS_PORT', 6379))
REDIS_USERNAME = os.getenv('REDIS_USERNAME', None)
REDIS_PASSWORD = os.getenv('REDIS_PASSWORD', None)
REDIS_DATABASES = {
'STATS': 0
}
MESSAGE_TAGS = {messages.ERROR: 'danger'} MESSAGE_TAGS = {messages.ERROR: 'danger'}
# Application definition # Application definition

View File

@@ -44,6 +44,7 @@ pyppeteer==2.0.0
pytube==15.0.0 pytube==15.0.0
aiohttp==3.10.2 aiohttp==3.10.2
inflection==0.5.1 inflection==0.5.1
redis==5.2.0
# Development # Development
pytest==8.0.0 pytest==8.0.0