diff --git a/djangoldp/models.py b/djangoldp/models.py index 8976d449d1af3c47848b69a6de53cae1352f37dc..2c5e45118a4130b8eaa889ec7c2c651d8fa8cb95 100644 --- a/djangoldp/models.py +++ b/djangoldp/models.py @@ -1,24 +1,23 @@ import json +import logging import uuid from urllib.parse import urlparse + from django.conf import settings -from django.contrib.auth.models import User -from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.contrib.auth import get_user_model +from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.db import models from django.db.models import BinaryField, DateTimeField from django.db.models.base import ModelBase -from django.db.models.signals import post_save +from django.db.models.signals import post_save, pre_save from django.dispatch import receiver -from django.urls import reverse_lazy, get_resolver +from django.urls import reverse_lazy, get_resolver, NoReverseMatch from django.utils.datastructures import MultiValueDictKeyError from django.utils.decorators import classonlymethod from rest_framework.utils import model_meta from djangoldp.fields import LDPUrlField from djangoldp.permissions import LDPPermissions -import logging - logger = logging.getLogger('djangoldp') @@ -322,7 +321,8 @@ class Activity(Model): # temporary database-side storage used for scheduled tasks in the ActivityQueue class ScheduledActivity(Activity): - failed_attempts = models.PositiveIntegerField(default=0, help_text='a log of how many failed retries have been made sending the activity') + failed_attempts = models.PositiveIntegerField(default=0, + help_text='a log of how many failed retries have been made sending the activity') def save(self, *args, **kwargs): self.is_finished = False @@ -361,9 +361,14 @@ if 'djangoldp_account' not in settings.DJANGOLDP_PACKAGES: webid = '{0}{1}'.format(settings.BASE_URL, reverse_lazy('user-detail', kwargs={'pk': self.pk})) return webid + get_user_model().webid = webid -@receiver(post_save, sender=User) -def update_perms(sender, instance, created, **kwargs): - LDPPermissions.invalidate_cache() +@receiver(pre_save) +def invalidate_caches(instance, **kwargs): + if isinstance(instance, Model): + from djangoldp.serializers import LDListMixin, LDPSerializer + LDPPermissions.invalidate_cache() + LDListMixin.to_representation_cache.reset() + LDPSerializer.to_representation_cache.reset() diff --git a/djangoldp/permissions.py b/djangoldp/permissions.py index c8f71aa846a2df552e5bfa5c1079c1cc98b73b41..1c3046cf64dffdcac4982027f588d71d7397b3c7 100644 --- a/djangoldp/permissions.py +++ b/djangoldp/permissions.py @@ -30,7 +30,7 @@ class LDPPermissions(DjangoObjectPermissions): @classmethod def refresh_cache(cls): - if time.time() - cls.perms_cache['time'] > 5: + if (time.time() - cls.perms_cache['time']) > 5: cls.invalidate_cache() def user_permissions(self, user, obj_or_model, obj=None): @@ -38,18 +38,15 @@ class LDPPermissions(DjangoObjectPermissions): Filter user permissions for a model class """ - # this may be a permission for the model class, or an instance self.refresh_cache() + # this may be a permission for the model class, or an instance if isinstance(obj_or_model, ModelBase): model = obj_or_model else: obj = obj_or_model model = obj_or_model.__class__ - model_name = model._meta.model_name - user_key = 'None' if user is None else user.id - obj_key = 'None' if obj is None else obj.id - perms_cache_key = 'User{}{}{}'.format(user_key, model_name, obj_key) + perms_cache_key = self.cache_key(model, obj, user) if self.with_cache and perms_cache_key in self.perms_cache: return self.perms_cache[perms_cache_key] @@ -95,7 +92,13 @@ class LDPPermissions(DjangoObjectPermissions): self.perms_cache[perms_cache_key] = list(perms) return self.perms_cache[perms_cache_key] - # return list(perms) + + def cache_key(self, model, obj, user): + model_name = model._meta.model_name + user_key = 'None' if user is None else user.id + obj_key = 'None' if obj is None else obj.id + perms_cache_key = 'User{}{}{}'.format(user_key, model_name, obj_key) + return perms_cache_key def filter_user_perms(self, context, obj_or_model, permissions): # Only used on Model.get_permissions to translate permissions to LDP diff --git a/djangoldp/serializers.py b/djangoldp/serializers.py index a9eee01c855a0c33e51c3af9db30d3f72537a9b4..28894baf0aa12d83c36472d2b91f2bca97d61423 100644 --- a/djangoldp/serializers.py +++ b/djangoldp/serializers.py @@ -1,4 +1,3 @@ -import time import uuid from collections import OrderedDict, Mapping, Iterable from typing import Any @@ -9,16 +8,17 @@ from django.contrib.auth import get_user_model from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ValidationError as DjangoValidationError from django.db import transaction +from django.db.models import QuerySet from django.urls import resolve, Resolver404, get_script_prefix from django.urls.resolvers import get_resolver -from django.db.models import QuerySet from django.utils.datastructures import MultiValueDictKeyError from django.utils.encoding import uri_to_iri from rest_framework.exceptions import ValidationError from rest_framework.fields import SkipField, empty, ReadOnlyField from rest_framework.fields import get_error_detail, set_value from rest_framework.relations import HyperlinkedRelatedField, ManyRelatedField, MANY_RELATION_KWARGS, Hyperlink -from rest_framework.serializers import HyperlinkedModelSerializer, ListSerializer, ModelSerializer, LIST_SERIALIZER_KWARGS +from rest_framework.serializers import HyperlinkedModelSerializer, ListSerializer, ModelSerializer, \ + LIST_SERIALIZER_KWARGS from rest_framework.settings import api_settings from rest_framework.utils import model_meta from rest_framework.utils.field_mapping import get_nested_relation_kwargs @@ -33,26 +33,34 @@ class InMemoryCache: def __init__(self): self.cache = { - 'time': time.time() } - def invalidate_cache(self): + def reset(self): self.cache = { - 'time': time.time() } - def refresh_cache(self): - if time.time() - self.cache['time'] > 5: - self.invalidate_cache() + def has(self, cache_key, vary): + if cache_key in self.cache and vary in self.cache[cache_key]: + return True + else: + return cache_key in self.cache - def has(self, cache_key): - return cache_key in self.cache + def get(self, cache_key, vary): + if self.has(cache_key, vary): + return self.cache[cache_key][vary]['value'] + else: + return None - def get(self, cache_key): - return self.cache[cache_key] + def set(self, cache_key, vary, value): + if cache_key not in self.cache: + self.cache[cache_key] = {} + self.cache[cache_key][vary] = {'value': value} - def set(self, cache_key, value): - self.cache[cache_key] = value + def invalidate(self, cache_key, vary=None): + if vary is None: + self.cache.pop(cache_key, None) + else: + self.cache[cache_key].pop(vary, None) class LDListMixin: @@ -88,7 +96,6 @@ class LDListMixin: - Can Add if add permission on contained object's type - Can view the container is view permission on container model : container obj are filtered by view permission ''' - self.to_representation_cache.refresh_cache() try: child_model = getattr(self, self.child_attr).Meta.model except AttributeError: @@ -96,6 +103,8 @@ class LDListMixin: parent_model = None + cache_vary = str(self.context['request'].user) + if isinstance(value, QuerySet): value = list(value) @@ -104,8 +113,8 @@ class LDListMixin: self.id = '{}{}{}'.format(settings.BASE_URL, Model.resource(parent_model), self.id) cache_key = self.id - if self.with_cache and self.to_representation_cache.has(cache_key): - return self.to_representation_cache.get(cache_key) + if self.with_cache and self.to_representation_cache.has(cache_key, cache_vary): + return self.to_representation_cache.get(cache_key, cache_vary) filtered_values = value container_permissions = Model.get_permissions(child_model, self.context, ['view', 'add']) @@ -121,8 +130,8 @@ class LDListMixin: self.id = '{}{}{}'.format(settings.BASE_URL, Model.resource(parent_model), self.id) cache_key = self.id - if self.with_cache and self.to_representation_cache.has(cache_key): - return self.to_representation_cache.get(cache_key) + if self.with_cache and self.to_representation_cache.has(cache_key, cache_vary): + return self.to_representation_cache.get(cache_key, cache_vary) # remove objects from the list which I don't have permission to view filtered_values = list( @@ -132,13 +141,14 @@ class LDListMixin: container_permissions.extend( Model.get_permissions(parent_model, self.context, ['view'])) - self.to_representation_cache.set(self.id, {'@id': self.id, - '@type': 'ldp:Container', - 'ldp:contains': super().to_representation(filtered_values), - 'permissions': container_permissions - }) + self.to_representation_cache.set(self.id, cache_vary, {'@id': self.id, + '@type': 'ldp:Container', + 'ldp:contains': super().to_representation( + filtered_values), + 'permissions': container_permissions + }) - return self.to_representation_cache.get(self.id) + return self.to_representation_cache.get(self.id, cache_vary) def get_attribute(self, instance): parent_id_field = self.parent.fields[self.parent.url_field_name] @@ -268,6 +278,7 @@ class JsonLdRelatedField(JsonLdField): class JsonLdIdentityField(JsonLdField): '''Represents an identity (url) field for a serializer''' + def __init__(self, view_name=None, **kwargs): kwargs['read_only'] = True kwargs['source'] = '*' @@ -323,16 +334,16 @@ class LDPSerializer(HyperlinkedModelSerializer): def to_representation(self, obj): # external Models should only be returned with an id (on GET) - self.to_representation_cache.refresh_cache() if self.context['request'].method == 'GET' and Model.is_external(obj): return {'@id': obj.urlid} + cache_vary = str(self.context['request'].user) if self.with_cache and hasattr(obj, 'urlid'): - if self.to_representation_cache.has(obj.urlid): - data = self.to_representation_cache.get(obj.urlid) + if self.to_representation_cache.has(obj.urlid, cache_vary): + data = self.to_representation_cache.get(obj.urlid, cache_vary) else: data = super().to_representation(obj) - self.to_representation_cache.set(obj.urlid, data) + self.to_representation_cache.set(obj.urlid, cache_vary, data) else: data = super().to_representation(obj) @@ -716,7 +727,7 @@ class LDPSerializer(HyperlinkedModelSerializer): info = model_meta.get_field_info(instance) slug_field = Model.slug_field(instance) relation_info = info.relations.get(attr) - if slug_field in value : + if slug_field in value: value = self.update_dict_value_when_id_is_provided(attr, instance, relation_info, slug_field, value) else: if 'urlid' in value: diff --git a/djangoldp/tests/perf_result.csv b/djangoldp/tests/perf_result.csv index 33d2f033490923f81080c5ac54527b537eb97bb3..c6281b8e0e6b9326faa9c8451eaf3f71d72037b9 100644 --- a/djangoldp/tests/perf_result.csv +++ b/djangoldp/tests/perf_result.csv @@ -46,4 +46,6 @@ jbl+AC0-T440p,Oct 09 2020 11:56:19,True,True,100,0.003119325637817,0.00560247182 jbl+AC0-T440p,Oct 09 2020 11:58:22,True,True,100,0.003008058071136,0.005401248931885,0.010658957958222,0.003909242153168,0.000718443393707,0.301162958145142,TRUE,3 jbl+AC0-T440p,Oct 09 2020 11:59:16,True,True,100,0.003015418052673,0.005526115894318,0.010740044116974,0.00400491476059,0.000724492073059,0.313828229904175,TRUE,4 jbl+AC0-T440p,Oct 09 2020 12:00:32,True,True,100,0.002969658374786,0.005434756278992,0.018136837482452,0.003030817508698,0.000726938247681,0.320115327835083,TRUE,0 -jbl-T440p,Oct 09 2020 12:21:00,True,True,100,0.0034934663772583007,0.0061032938957214355,0.019232537746429443,0.003091294765472412,0.0007375502586364747,0.36986708641052246,N/A +jbl+AC0-T440p,Oct 09 2020 12:21:00,True,True,100,0.003493466377258,0.006103293895721,0.01923253774643,0.003091294765472,0.000737550258636,0.369867086410522,TRUE,0 +jbl+AC0-T440p,Oct 15 2020 22:00:10,True,True,100,0.003004941940308,0.00546817779541,0.018348352909088,0.003068554401398,0.000729415416718,0.320573329925537,TRUE,0 +jbl+AC0-T440p,Oct 15 2020 22:15:26,True,True,100,0.003350086212158,0.005898218154907,0.011625332832337,0.004264788627625,0.000795011520386,0.319289922714233,TRUE,1 diff --git a/djangoldp/tests/runner.py b/djangoldp/tests/runner.py index 63cb160784dac942a2224dde6902205eff4f676c..5b432cc4db424c8966e2d98c7501ae946a78123f 100644 --- a/djangoldp/tests/runner.py +++ b/djangoldp/tests/runner.py @@ -25,7 +25,8 @@ failures = test_runner.run_tests([ 'djangoldp.tests.tests_sources', 'djangoldp.tests.tests_pagination', 'djangoldp.tests.tests_inbox', - 'djangoldp.tests.tests_backlinks_service' + 'djangoldp.tests.tests_backlinks_service', + 'djangoldp.tests.tests_cache' ]) if failures: diff --git a/djangoldp/tests/tests_cache.py b/djangoldp/tests/tests_cache.py new file mode 100644 index 0000000000000000000000000000000000000000..ff4aed657bae7be0b803a4b863e2add45fb52d56 --- /dev/null +++ b/djangoldp/tests/tests_cache.py @@ -0,0 +1,73 @@ +from django.contrib.auth import get_user_model +from django.test import TestCase +from rest_framework.test import APIRequestFactory, APIClient +from rest_framework.utils import json + +from djangoldp.models import Model +from djangoldp.serializers import LDPSerializer, LDListMixin +from djangoldp.tests.models import Skill, JobOffer, Invoice, LDPDummy, Resource, Post, Circle, Project, Conversation + + +class TestCache(TestCase): + + def setUp(self): + self.factory = APIRequestFactory() + self.client = APIClient() + self.user = get_user_model().objects.create_user(username='john', email='jlennon@beatles.com', + password='glass onion') + self.client.force_authenticate(self.user) + LDListMixin.to_representation_cache.reset() + LDPSerializer.to_representation_cache.reset() + + def tearDown(self): + pass + + def test_save_fk_graph_with_nested(self): + response = self.client.get('/batchs/', content_type='application/ld+json') + self.assertEqual(response.status_code, 200) + + post = { + '@graph': [ + { + 'http://happy-dev.fr/owl/#title': "title", + 'http://happy-dev.fr/owl/#invoice': { + '@id': "_.123" + } + }, + { + '@id': "_.123", + 'http://happy-dev.fr/owl/#title': "title 2" + } + ] + } + + response = self.client.post('/batchs/', data=json.dumps(post), content_type='application/ld+json') + self.assertEqual(response.status_code, 201) + + response = self.client.get('/batchs/', content_type='application/ld+json') + self.assertIn('ldp:contains', response.data) + self.assertEquals(response.data['ldp:contains'][0]['title'], "title") + self.assertEquals(response.data['ldp:contains'][0]['invoice']['title'], "title 2") + + def test_update_with_new_fk_relation(self): + conversation = Conversation.objects.create(author_user=self.user, + description="conversation description") + response = self.client.get('/conversations/{}/'.format(conversation.pk), content_type='application/ld+json') + body = [ + { + '@id': "/conversations/{}/".format(conversation.pk), + 'http://happy-dev.fr/owl/#description': "conversation update", + 'http://happy-dev.fr/owl/#peer_user': { + '@id': 'http://happy-dev.fr/users/{}'.format(self.user.pk), + } + } + ] + response = self.client.put('/conversations/{}/'.format(conversation.pk), data=json.dumps(body), + content_type='application/ld+json') + self.assertEqual(response.status_code, 200) + + response = self.client.get('/conversations/{}/'.format(conversation.pk), content_type='application/ld+json') + self.assertIn('peer_user', response.data) + self.assertEquals('conversation update', response.data['description']) + self.assertIn('@id', response.data['peer_user']) + diff --git a/djangoldp/tests/tests_get.py b/djangoldp/tests/tests_get.py index 96baf36e5e287d835eba632d20eea34f95f19cd9..7ae5c76f069a003f351bb4806c3e18651a7f8dff 100644 --- a/djangoldp/tests/tests_get.py +++ b/djangoldp/tests/tests_get.py @@ -11,8 +11,8 @@ class TestGET(APITestCase): def setUp(self): self.factory = APIRequestFactory() self.client = APIClient() - LDListMixin.to_representation_cache.invalidate_cache() - LDPSerializer.to_representation_cache.invalidate_cache() + LDListMixin.to_representation_cache.reset() + LDPSerializer.to_representation_cache.reset() def tearDown(self): pass diff --git a/djangoldp/tests/tests_perf_get.py b/djangoldp/tests/tests_perf_get.py index 4b1ca1eda35c450348f5a6b2b9c76a21f565624a..1120e1cccce09c7c2650e02fbc1be540ec633d5a 100644 --- a/djangoldp/tests/tests_perf_get.py +++ b/djangoldp/tests/tests_perf_get.py @@ -18,6 +18,7 @@ class TestPerformanceGET(APITestCase): result_line = [] withAuth = True withPermsCache = True + # fixtures = ['test_ten_1000.json',] @classmethod def setUpClass(cls): diff --git a/djangoldp/tests/tests_save.py b/djangoldp/tests/tests_save.py index 92255e46536e3b3147266aa179b9a32a9153757a..bebcb4acf940bce37ddd2de2b706da3f34fbdff6 100644 --- a/djangoldp/tests/tests_save.py +++ b/djangoldp/tests/tests_save.py @@ -16,8 +16,8 @@ class Save(TestCase): self.user = get_user_model().objects.create_user(username='john', email='jlennon@beatles.com', password='glass onion') self.client.force_authenticate(self.user) - LDListMixin.to_representation_cache.invalidate_cache() - LDPSerializer.to_representation_cache.invalidate_cache() + LDListMixin.to_representation_cache.reset() + LDPSerializer.to_representation_cache.reset() def tearDown(self): pass diff --git a/djangoldp/tests/tests_update.py b/djangoldp/tests/tests_update.py index ea293c3a4118af7889fcca3224e71e66002b88a7..a4b6ef01f8d0351be43322f36c9e1badba4dc873 100644 --- a/djangoldp/tests/tests_update.py +++ b/djangoldp/tests/tests_update.py @@ -17,8 +17,8 @@ class Update(TestCase): self.user = get_user_model().objects.create_user(username='john', email='jlennon@beatles.com', password='glass onion') self.client.force_authenticate(user=self.user) - LDListMixin.to_representation_cache.invalidate_cache() - LDPSerializer.to_representation_cache.invalidate_cache() + LDListMixin.to_representation_cache.reset() + LDPSerializer.to_representation_cache.reset() def tearDown(self): pass