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

@@ -12,6 +12,7 @@ from json import JSONDecodeError
from urllib.parse import unquote
from zipfile import ZipFile
import redis
import requests
from PIL import UnidentifiedImageError
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.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 icalendar import Calendar, Event
@@ -101,6 +103,46 @@ from recipes import settings
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):
def get_queryset(self):
@@ -184,9 +226,9 @@ class FuzzyFilterMixin(ViewSetMixin, ExtendedRecipeMixin):
if query is not None and query not in ["''", '']:
if fuzzy and (settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql'):
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.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name__unaccent', query))
[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))
else:
self.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name', query))
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)
class UserViewSet(viewsets.ModelViewSet):
class UserViewSet(LoggingMixin, viewsets.ModelViewSet):
"""
list:
optional parameters
@@ -394,14 +436,14 @@ 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]
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]
@@ -411,7 +453,7 @@ class SpaceViewSet(viewsets.ModelViewSet):
return self.queryset.filter(id=self.request.space.id)
class UserSpaceViewSet(viewsets.ModelViewSet):
class UserSpaceViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = UserSpace.objects
serializer_class = UserSpaceSerializer
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)
class UserPreferenceViewSet(viewsets.ModelViewSet):
class UserPreferenceViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = UserPreference.objects
serializer_class = UserPreferenceSerializer
permission_classes = [CustomIsOwner & CustomTokenHasReadWriteScope]
@@ -445,7 +487,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
@@ -455,7 +497,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]
@@ -464,7 +506,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]
@@ -473,7 +515,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]
@@ -483,7 +525,7 @@ class SyncLogViewSet(viewsets.ReadOnlyModelViewSet):
return self.queryset.filter(sync__space=self.request.space)
class SupermarketViewSet(viewsets.ModelViewSet, StandardFilterMixin):
class SupermarketViewSet(LoggingMixin, viewsets.ModelViewSet, StandardFilterMixin):
schema = FilterSchema()
queryset = Supermarket.objects
serializer_class = SupermarketSerializer
@@ -494,7 +536,7 @@ class SupermarketViewSet(viewsets.ModelViewSet, StandardFilterMixin):
return super().get_queryset()
class SupermarketCategoryViewSet(viewsets.ModelViewSet, FuzzyFilterMixin, MergeMixin):
class SupermarketCategoryViewSet(LoggingMixin, viewsets.ModelViewSet, FuzzyFilterMixin, MergeMixin):
queryset = SupermarketCategory.objects
model = SupermarketCategory
serializer_class = SupermarketCategorySerializer
@@ -505,7 +547,7 @@ class SupermarketCategoryViewSet(viewsets.ModelViewSet, FuzzyFilterMixin, MergeM
return super().get_queryset()
class SupermarketCategoryRelationViewSet(viewsets.ModelViewSet, StandardFilterMixin):
class SupermarketCategoryRelationViewSet(LoggingMixin, viewsets.ModelViewSet, StandardFilterMixin):
queryset = SupermarketCategoryRelation.objects
serializer_class = SupermarketCategoryRelationSerializer
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
@@ -516,7 +558,7 @@ class SupermarketCategoryRelationViewSet(viewsets.ModelViewSet, StandardFilterMi
return super().get_queryset()
class KeywordViewSet(viewsets.ModelViewSet, TreeMixin):
class KeywordViewSet(LoggingMixin, viewsets.ModelViewSet, TreeMixin):
queryset = Keyword.objects
model = Keyword
serializer_class = KeywordSerializer
@@ -524,7 +566,7 @@ class KeywordViewSet(viewsets.ModelViewSet, TreeMixin):
pagination_class = DefaultPagination
class UnitViewSet(viewsets.ModelViewSet, MergeMixin, FuzzyFilterMixin):
class UnitViewSet(LoggingMixin, viewsets.ModelViewSet, MergeMixin, FuzzyFilterMixin):
queryset = Unit.objects
model = Unit
serializer_class = UnitSerializer
@@ -532,7 +574,7 @@ class UnitViewSet(viewsets.ModelViewSet, MergeMixin, FuzzyFilterMixin):
pagination_class = DefaultPagination
class FoodInheritFieldViewSet(viewsets.ReadOnlyModelViewSet):
class FoodInheritFieldViewSet(LoggingMixin, viewsets.ReadOnlyModelViewSet):
queryset = FoodInheritField.objects
serializer_class = FoodInheritFieldSerializer
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
@@ -543,7 +585,7 @@ class FoodInheritFieldViewSet(viewsets.ReadOnlyModelViewSet):
return super().get_queryset()
class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
class FoodViewSet(LoggingMixin, viewsets.ModelViewSet, TreeMixin):
queryset = Food.objects
model = Food
serializer_class = FoodSerializer
@@ -610,8 +652,8 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
return JsonResponse(
{
'msg':
'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.'
'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.'
},
status=429,
json_dumps_params={'indent': 4})
@@ -680,7 +722,7 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
return Response(content, status=status.HTTP_403_FORBIDDEN)
class RecipeBookViewSet(viewsets.ModelViewSet, StandardFilterMixin):
class RecipeBookViewSet(LoggingMixin, viewsets.ModelViewSet, StandardFilterMixin):
queryset = RecipeBook.objects
serializer_class = RecipeBookSerializer
permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope]
@@ -698,7 +740,7 @@ class RecipeBookViewSet(viewsets.ModelViewSet, StandardFilterMixin):
return super().get_queryset()
class RecipeBookEntryViewSet(viewsets.ModelViewSet, viewsets.GenericViewSet):
class RecipeBookEntryViewSet(LoggingMixin, viewsets.ModelViewSet, viewsets.GenericViewSet):
"""
list:
optional parameters
@@ -724,7 +766,7 @@ class RecipeBookEntryViewSet(viewsets.ModelViewSet, viewsets.GenericViewSet):
return queryset
class MealPlanViewSet(viewsets.ModelViewSet):
class MealPlanViewSet(LoggingMixin, viewsets.ModelViewSet):
"""
list:
optional parameters
@@ -762,7 +804,7 @@ class MealPlanViewSet(viewsets.ModelViewSet):
return queryset
class AutoPlanViewSet(viewsets.ViewSet):
class AutoPlanViewSet(LoggingMixin, viewsets.ViewSet):
def create(self, request):
serializer = AutoMealPlanSerializer(data=request.data)
@@ -824,7 +866,7 @@ class AutoPlanViewSet(viewsets.ViewSet):
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.
@@ -838,7 +880,7 @@ class MealTypeViewSet(viewsets.ModelViewSet):
return queryset
class IngredientViewSet(viewsets.ModelViewSet):
class IngredientViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = Ingredient.objects
serializer_class = IngredientSerializer
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
@@ -862,7 +904,7 @@ class IngredientViewSet(viewsets.ModelViewSet):
return queryset.select_related('food')
class StepViewSet(viewsets.ModelViewSet):
class StepViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = Step.objects
serializer_class = StepSerializer
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), ]))
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
@@ -1064,7 +1106,7 @@ class RecipeViewSet(viewsets.ModelViewSet):
return Response(self.serializer_class(qs, many=True).data)
class UnitConversionViewSet(viewsets.ModelViewSet):
class UnitConversionViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = UnitConversion.objects
serializer_class = UnitConversionSerializer
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
@@ -1081,7 +1123,7 @@ class UnitConversionViewSet(viewsets.ModelViewSet):
return self.queryset.filter(space=self.request.space)
class PropertyTypeViewSet(viewsets.ModelViewSet):
class PropertyTypeViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = PropertyType.objects
serializer_class = PropertyTypeSerializer
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
@@ -1090,7 +1132,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]
@@ -1099,7 +1141,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]
@@ -1110,10 +1152,10 @@ class ShoppingListRecipeViewSet(viewsets.ModelViewSet):
Q(entries__isnull=True)
| Q(entries__created_by=self.request.user)
| 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
serializer_class = ShoppingListEntrySerializer
permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope]
@@ -1123,7 +1165,7 @@ class ShoppingListEntryViewSet(viewsets.ModelViewSet):
name='checked',
description=_('Filter shopping list entries on checked. [''true'', ''false'', ''both'', ''<b>recent</b>'']<br> \
- ''recent'' includes unchecked items and recently completed items.')
),
),
QueryParam(name='supermarket', description=_('Returns the shopping list entries sorted by supermarket category order.'), qtype='integer'),
]
schema = QueryParamAutoSchema()
@@ -1171,7 +1213,7 @@ class ShoppingListEntryViewSet(viewsets.ModelViewSet):
print(serializer.validated_data)
bulk_entries = ShoppingListEntry.objects.filter(
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(), )
# update the onhand for food if shopping_add_onhand is True
@@ -1189,7 +1231,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]
@@ -1200,7 +1242,7 @@ class ViewLogViewSet(viewsets.ModelViewSet):
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
serializer_class = CookLogSerializer
permission_classes = [CustomIsOwner & CustomTokenHasReadWriteScope]
@@ -1215,7 +1257,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]
@@ -1225,7 +1267,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]
@@ -1235,7 +1277,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]
@@ -1250,7 +1292,7 @@ class BookmarkletImportViewSet(viewsets.ModelViewSet):
return self.queryset.filter(space=self.request.space).all()
class UserFileViewSet(viewsets.ModelViewSet, StandardFilterMixin):
class UserFileViewSet(LoggingMixin, viewsets.ModelViewSet, StandardFilterMixin):
schema = FilterSchema()
queryset = UserFile.objects
serializer_class = UserFileSerializer
@@ -1262,7 +1304,7 @@ class UserFileViewSet(viewsets.ModelViewSet, StandardFilterMixin):
return super().get_queryset()
class AutomationViewSet(viewsets.ModelViewSet, StandardFilterMixin):
class AutomationViewSet(LoggingMixin, viewsets.ModelViewSet, StandardFilterMixin):
"""
list:
optional parameters
@@ -1313,7 +1355,7 @@ class AutomationViewSet(viewsets.ModelViewSet, StandardFilterMixin):
return super().get_queryset()
class InviteLinkViewSet(viewsets.ModelViewSet, StandardFilterMixin):
class InviteLinkViewSet(LoggingMixin, viewsets.ModelViewSet, StandardFilterMixin):
queryset = InviteLink.objects
serializer_class = InviteLinkSerializer
permission_classes = [CustomIsSpaceOwner & CustomIsAdmin & CustomTokenHasReadWriteScope]
@@ -1331,7 +1373,7 @@ class InviteLinkViewSet(viewsets.ModelViewSet, StandardFilterMixin):
return None
class CustomFilterViewSet(viewsets.ModelViewSet, StandardFilterMixin):
class CustomFilterViewSet(LoggingMixin, viewsets.ModelViewSet, StandardFilterMixin):
queryset = CustomFilter.objects
serializer_class = CustomFilterSerializer
permission_classes = [CustomIsOwner & CustomTokenHasReadWriteScope]
@@ -1342,7 +1384,7 @@ class CustomFilterViewSet(viewsets.ModelViewSet, StandardFilterMixin):
return super().get_queryset()
class AccessTokenViewSet(viewsets.ModelViewSet):
class AccessTokenViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = AccessToken.objects
serializer_class = AccessTokenSerializer
permission_classes = [CustomIsOwner & CustomTokenHasReadWriteScope]
@@ -1467,7 +1509,7 @@ class RecipeUrlImportView(APIView):
'recipe_json': helper.get_from_scraper(scrape, request),
'recipe_images': list(dict.fromkeys(get_images_from_soup(scrape.soup, url))),
},
status=status.HTTP_200_OK)
status=status.HTTP_200_OK)
else:
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
@permission_classes([CustomIsUser & CustomTokenHasReadWriteScope])
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)
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]))})