diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 137fb381afb68daedc80ff91318b00c249ef8a82..0e31444c03f5950e534f9e291ae735258c967755 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -8,7 +8,8 @@ stages: test: stage: test script: - - echo 'Make your tests here !' + - pip install .[dev] + - python -m unittest djangoldp.tests.runner except: - master tags: diff --git a/README.md b/README.md index a565c8354947546fef8580a46dc421bde133241c..07ac1cf2a49059c4e42967fae86544e5decf6264 100644 --- a/README.md +++ b/README.md @@ -27,11 +27,14 @@ django-admin startproject myldpserver ``` 3. Create your django model inside a file myldpserver/myldpserver/models.py +Note that container_path will be use to resolve instance iri and container iri +In the future it could also be used to auto configure django router (e.g. urls.py) ``` -from django.db import models +from djangoldp.models import Model -class Todo(models.Model): +class Todo(Model): + container_path = "/my-path/" name = models.CharField(max_length=255) deadline = models.DateTimeField() diff --git a/djangoldp/models.py b/djangoldp/models.py index 02503fb212486c911a20f89112fa6a62700f11e1..6e02705c724270e6299eb2010c8d93e383f76e23 100644 --- a/djangoldp/models.py +++ b/djangoldp/models.py @@ -1,16 +1,86 @@ from django.conf import settings from django.db import models +from django.urls import get_resolver from rest_framework import fields + +class Model(models.Model): + container_path = None + + def get_container_path(self): + return self.container_path + + def get_absolute_url(self): + return Model.resource_id(self) + + def get_container_id(self): + return Model.container_id(self) + + @classmethod + def resource_id(cls, instance): + view_name = '{}-detail'.format(instance._meta.object_name.lower()) + slug_field = '/{}'.format(get_resolver().reverse_dict[view_name][0][0][1][0]) + if slug_field.startswith('/'): + slug_field = slug_field[1:] + return "{}{}".format(cls.container_id(instance), getattr(instance, slug_field)) + + @classmethod + def container_id(cls, instance): + if isinstance(instance, cls): + path = instance.container_path + if path is None: + path = "{}s".format(instance._meta.object_name.lower()) + else: + view_name = '{}-list'.format(instance._meta.object_name.lower()) + path = get_resolver().reverse(view_name) + + path = cls.__clean_path(path) + + return path + + class Meta: + abstract = True + + @classmethod + def resolve_id(cls, id): + id = cls.__clean_path(id) + view, args, kwargs = get_resolver().resolve(id) + return view.initkwargs['model'].objects.get(**kwargs) + + @classmethod + def resolve_container(cls, path): + path = cls.__clean_path(path) + view, args, kwargs = get_resolver().resolve(path) + return view.initkwargs['model'] + + @classmethod + def resolve(cls, path): + container = cls.resolve_container(path) + try: + resolve_id = cls.resolve_id(path) + except: + resolve_id = None + return container, resolve_id + + @classmethod + def __clean_path(cls, path): + if not path.startswith("/"): + path = "/{}".format(path) + if not path.endswith("/"): + path = "{}/".format(path) + return path + + class LDPUrlField (fields.URLField): def to_representation(self, value): str = super(LDPUrlField, self).to_representation(value) return {'@id': str} + class LDPSource(models.Model): container = models.URLField() federation = models.CharField(max_length=255) - + class Meta: rdf_type = 'sib:source' ordering = ('federation',) @@ -18,7 +88,7 @@ class LDPSource(models.Model): ('view_source', 'acl:Read'), ('control_source', 'acl:Control'), ) - + def __str__(self): return "{}: {}".format(self.federation, self.container) @@ -30,6 +100,7 @@ class LDNotification(models.Model): type = models.CharField(max_length=255) summary = models.TextField() date = models.DateTimeField(auto_now_add=True) + class Meta: permissions = ( ('view_todo', 'Read'), diff --git a/djangoldp/serializers.py b/djangoldp/serializers.py index 41bb34d43eb2b69c119e009e70ab1f7ca870cb1f..8f68daec47f465da5e069173e9d976f2dfb8592a 100644 --- a/djangoldp/serializers.py +++ b/djangoldp/serializers.py @@ -1,9 +1,23 @@ +from collections import OrderedDict, Mapping +from urllib import parse + from django.core.exceptions import ImproperlyConfigured -from django.core.urlresolvers import get_resolver +from django.core.exceptions import ValidationError as DjangoValidationError +from django.core.urlresolvers import get_resolver, resolve, get_script_prefix, Resolver404 from django.utils.datastructures import MultiValueDictKeyError +from django.utils.encoding import uri_to_iri from guardian.shortcuts import get_perms -from rest_framework.fields import empty +from rest_framework.exceptions import ValidationError +from rest_framework.fields import SkipField, empty +from rest_framework.fields import get_error_detail, set_value from rest_framework.relations import HyperlinkedRelatedField, ManyRelatedField, MANY_RELATION_KWARGS +from rest_framework.serializers import HyperlinkedModelSerializer, ListSerializer +from rest_framework.settings import api_settings +from rest_framework.utils.field_mapping import get_nested_relation_kwargs +from rest_framework.utils.serializer_helpers import ReturnDict + +from djangoldp.models import Model + from rest_framework.serializers import HyperlinkedModelSerializer, ListSerializer, ModelSerializer from rest_framework.utils.field_mapping import get_nested_relation_kwargs from rest_framework.utils.serializer_helpers import ReturnDict @@ -13,17 +27,19 @@ from djangoldp import models class LDListMixin: def to_internal_value(self, data): - # data = json.loads(data) try: data = data['ldp:contains'] - except TypeError: + except (TypeError, KeyError): pass if isinstance(data, dict): data = [data] + if isinstance(data, str) and str.startswith("http"): + data = [{'@id': data}] return [self.child.to_internal_value(item) for item in data] def to_representation(self, value): return {'@id': self.id, '@type': 'ldp:Container', 'ldp:contains': super().to_representation(value)} + def get_attribute(self, instance): parent_id_field = self.parent.fields[self.parent.url_field_name] context = self.parent.context @@ -31,6 +47,41 @@ class LDListMixin: self.id = parent_id + self.field_name + "/" return super().get_attribute(instance) + def get_value(self, dictionary): + try: + object_list = dictionary["@graph"] + container_id = Model.container_id(self.parent.instance) + obj = next(filter(lambda o: container_id in o['@id'], object_list)) + list = super().get_value(obj) + try: + list = next(filter(lambda o: list['@id'] == o['@id'], object_list)) + except (KeyError, TypeError): + pass + + try: + list = list['ldp:contains'] + except (KeyError, TypeError): + pass + + if isinstance(list, dict): + list = [list] + + ret = [] + for item in list: + full_item = None + try: + full_item = next(filter(lambda o: item['@id'] == o['@id'], object_list)) + except StopIteration: + pass + if full_item is None: + ret.append(item) + else: + ret.append(full_item) + + return ret + except KeyError: + return super().get_value(dictionary) + class ContainerSerializer(LDListMixin, ListSerializer): id = '' @@ -45,7 +96,7 @@ class ContainerSerializer(LDListMixin, ListSerializer): def to_internal_value(self, data): try: return super().to_internal_value(data['@id']) - except: + except (KeyError, TypeError): return super().to_internal_value(data) @@ -66,6 +117,12 @@ class JsonLdField(HyperlinkedRelatedField): except MultiValueDictKeyError: pass + def to_internal_value(self, data): + return super().to_internal_value(data) + + def get_value(self, dictionary): + return super().get_value(dictionary) + class JsonLdRelatedField(JsonLdField): def to_representation(self, value): @@ -77,7 +134,7 @@ class JsonLdRelatedField(JsonLdField): def to_internal_value(self, data): try: return super().to_internal_value(data['@id']) - except: + except (KeyError, TypeError): return super().to_internal_value(data) @classmethod @@ -101,17 +158,24 @@ class JsonLdIdentityField(JsonLdField): def to_internal_value(self, data): try: return super().to_internal_value(data['@id']) - except: + except KeyError: return super().to_internal_value(data) + def get_value(self, dictionary): + return super().get_value(dictionary) + class LDPSerializer(HyperlinkedModelSerializer): url_field_name = "@id" serializer_related_field = JsonLdRelatedField serializer_url_field = JsonLdIdentityField - ModelSerializer.serializer_field_mapping [django_models.URLField] = models.LDPUrlField + + @property + def data(self): + return super().data + def get_default_field_names(self, declared_fields, model_info): try: fields = list(self.Meta.model._meta.serializer_fields) @@ -134,6 +198,27 @@ class LDPSerializer(HyperlinkedModelSerializer): get_perms(self.context['request'].user, obj)] return data + def build_standard_field(self, field_name, model_field): + class JSonLDStandardField: + parent_view_name = None + + def __init__(self, **kwargs): + self.parent_view_name = kwargs.pop('parent_view_name') + super().__init__(**kwargs) + + def get_value(self, dictionary): + try: + object_list = dictionary["@graph"] + resource_id = Model.resource_id(self.parent.instance) + obj = next(filter(lambda o: resource_id in o['@id'], object_list)) + return super().get_value(obj) + except KeyError: + return super().get_value(dictionary) + + field_class, field_kwargs = super().build_standard_field(field_name, model_field) + field_kwargs['parent_view_name'] = '{}-list'.format(model_field.model._meta.object_name.lower()) + return type(field_class.__name__ + 'Valued', (JSonLDStandardField, field_class), {}), field_kwargs + def build_nested_field(self, field_name, relation_info, nested_depth): class NestedLDPSerializer(self.__class__): @@ -146,10 +231,56 @@ class LDPSerializer(HyperlinkedModelSerializer): fields = '__all__' def to_internal_value(self, data): - model = self.Meta.model - return self.serializer_related_field( - view_name='{}-detail'.format(model._meta.object_name.lower()), - queryset=model.objects.all()).to_internal_value(data) + if self.url_field_name in data: + if not isinstance(data, Mapping): + message = self.error_messages['invalid'].format( + datatype=type(data).__name__ + ) + raise ValidationError({ + api_settings.NON_FIELD_ERRORS_KEY: [message] + }, code='invalid') + + ret = OrderedDict() + errors = OrderedDict() + fields = list(filter(lambda x: x.field_name in data, self._writable_fields)) + + for field in fields: + validate_method = getattr(self, 'validate_' + field.field_name, None) + primitive_value = field.get_value(data) + try: + validated_value = field.run_validation(primitive_value) + if validate_method is not None: + validated_value = validate_method(validated_value) + except ValidationError as exc: + errors[field.field_name] = exc.detail + except DjangoValidationError as exc: + errors[field.field_name] = get_error_detail(exc) + except SkipField: + pass + else: + set_value(ret, field.source_attrs, validated_value) + + if errors: + raise ValidationError(errors) + + uri = data[self.url_field_name] + http_prefix = uri.startswith(('http:', 'https:')) + + if http_prefix: + uri = parse.urlparse(uri).path + prefix = get_script_prefix() + if uri.startswith(prefix): + uri = '/' + uri[len(prefix):] + + try: + match = resolve(uri_to_iri(uri)) + ret['pk'] = match.kwargs['pk'] + except Resolver404: + pass + + return ret + else: + return super().to_internal_value(data) kwargs = get_nested_relation_kwargs(relation_info) kwargs['read_only'] = False @@ -158,19 +289,96 @@ class LDPSerializer(HyperlinkedModelSerializer): @classmethod def many_init(cls, *args, **kwargs): - kwargs['child'] = cls() + kwargs['child'] = cls(**kwargs) + try: + cls.Meta.depth = kwargs['context']['view'].many_depth + except KeyError: + pass return ContainerSerializer(*args, **kwargs) + def get_value(self, dictionary): + try: + object_list = dictionary["@graph"] + container_id = Model.container_path(self.parent.instance) + obj = next(filter(lambda o: container_id in o[self.url_field_name], object_list)) + item = super().get_value(obj) + full_item = None + if item is empty: + return empty + try: + full_item = next(filter(lambda o: item['@id'] == o['@id'], object_list)) + except StopIteration: + pass + if full_item is None: + return item + else: + return full_item + + except KeyError: + return super().get_value(dictionary) + def create(self, validated_data): + return self.internal_create(validated_data, model=self.Meta.model) + + def internal_create(self, validated_data, model): nested_fields = [] nested_fields_name = list(filter(lambda key: isinstance(validated_data[key], list), validated_data)) for field_name in nested_fields_name: nested_fields.append((field_name, validated_data.pop(field_name))) - obj = self.Meta.model.objects.create(**validated_data) + instance = model.objects.create(**validated_data) + + self.save_or_update_nested_list(instance, nested_fields) + + return instance + def update(self, instance, validated_data): + nested_fields = [] + nested_fields_name = list(filter(lambda key: isinstance(validated_data[key], list), validated_data)) + for field_name in nested_fields_name: + nested_fields.append((field_name, validated_data.pop(field_name))) + + for attr, value in validated_data.items(): + if isinstance(value, dict): + manager = getattr(instance, attr) + if 'pk' in value: + oldObj = manager._meta.model.objects.get(pk=value['pk']) + value = self.update(instance=oldObj, validated_data=value) + else: + value = self.internal_create(validated_data=value, model=manager._meta.model) + setattr(instance, attr, value) + + instance.save() + + self.save_or_update_nested_list(instance, nested_fields) + + return instance + + def save_or_update_nested_list(self, instance, nested_fields): for (field_name, data) in nested_fields: - for item in data: - getattr(obj, field_name).add(item) + manager = getattr(instance, field_name) - return obj + item_pk_to_keep = list(map(lambda e: int(e['pk']), filter(lambda x: 'pk' in x, data))) + for item in list(manager.all()): + if not item.pk in item_pk_to_keep: + if getattr(manager, 'through', None) is None: + item.delete() + else: + manager.remove(item) + + for item in data: + if 'pk' in item: + oldObj = manager.model.objects.get(pk=item['pk']) + savedItem = self.update(instance=oldObj, validated_data=item) + else: + rel = getattr(instance._meta.model, field_name).rel + try: + if rel.related_model == manager.model: + reverse_id = rel.remote_field.attname + item[reverse_id] = instance.pk + except AttributeError: + pass + savedItem = self.internal_create(validated_data=item, model=manager.model) + + if getattr(manager, 'through', None) is not None and manager.through._meta.auto_created: + manager.add(savedItem) diff --git a/djangoldp/tests/models.py b/djangoldp/tests/models.py index d8e0e76be8cc145c7dcc45635a4a342037dcc9a9..a68a803c1f26baa1bdb47ddd2c22706666b4c07f 100644 --- a/djangoldp/tests/models.py +++ b/djangoldp/tests/models.py @@ -1,12 +1,34 @@ from django.conf import settings from django.db import models +from djangoldp.models import Model + class Skill(models.Model): title = models.CharField(max_length=255, blank=True, null=True) + obligatoire = models.CharField(max_length=255) class JobOffer(models.Model): title = models.CharField(max_length=255, blank=True, null=True) skills = models.ManyToManyField(Skill, blank=True) + +class Thread(models.Model): + description = models.CharField(max_length=255, blank=True, null=True) + author_user = models.ForeignKey(settings.AUTH_USER_MODEL) + + +class Message(models.Model): + text = models.CharField(max_length=255, blank=True, null=True) + thread = models.ForeignKey(Thread, on_delete=models.DO_NOTHING) + author_user = models.ForeignKey(settings.AUTH_USER_MODEL) + + +class Dummy(models.Model): + some = models.CharField(max_length=255, blank=True, null=True) + + +class LDPDummy(Model): + some = models.CharField(max_length=255, blank=True, null=True) + diff --git a/djangoldp/tests/runner.py b/djangoldp/tests/runner.py index 8e04314473e2e7b8898ed9f3fa004daa303dc121..70074c5b66755ae9ffe7715611f1e2c3335c0945 100644 --- a/djangoldp/tests/runner.py +++ b/djangoldp/tests/runner.py @@ -24,6 +24,12 @@ from django.test.runner import DiscoverRunner test_runner = DiscoverRunner(verbosity=1) -failures = test_runner.run_tests(['djangoldp.tests.tests']) +failures = test_runner.run_tests([ + 'djangoldp.tests.tests_ldp_model', + 'djangoldp.tests.tests_save', + 'djangoldp.tests.tests_user_permissions', + 'djangoldp.tests.tests_anonymous_permissions', + 'djangoldp.tests.tests_update']) if failures: sys.exit(failures) + diff --git a/djangoldp/tests/tests.py b/djangoldp/tests/tests.py deleted file mode 100644 index fe5de882868459b37beb4d3de5d5b0d0e8a15f6d..0000000000000000000000000000000000000000 --- a/djangoldp/tests/tests.py +++ /dev/null @@ -1,122 +0,0 @@ -from django.test import TestCase - -from djangoldp.serializers import LDPSerializer -from djangoldp.tests.models import Skill, JobOffer - - -class Serializer(TestCase): - - def test_container_serializer_save(self): - skill1 = Skill.objects.create(title="skill1") - skill2 = Skill.objects.create(title="skill2") - job = {"title": "job test", - "skills": { - "ldp:contains": [ - {"@id": "https://happy-dev.fr/skills/{}/".format(skill1.pk)}, - {"@id": "https://happy-dev.fr/skills/{}/".format(skill2.pk)}, - ]} - } - - meta_args = {'model': JobOffer, 'depth': 1, 'fields': ("@id", "title", "skills")} - - meta_class = type('Meta', (), meta_args) - serializer_class = type(LDPSerializer)('JobOfferSerializer', (LDPSerializer,), {'Meta': meta_class}) - serializer = serializer_class(data=job) - serializer.is_valid() - result = serializer.save() - - self.assertEquals(result.title, "job test") - self.assertIs(result.skills.count(), 2) - - def test_save_without_nested_fields(self): - skill1 = Skill.objects.create(title="skill1") - skill2 = Skill.objects.create(title="skill2") - job = {"title": "job test"} - - meta_args = {'model': JobOffer, 'depth': 1, 'fields': ("@id", "title", "skills")} - - meta_class = type('Meta', (), meta_args) - serializer_class = type(LDPSerializer)('JobOfferSerializer', (LDPSerializer,), {'Meta': meta_class}) - serializer = serializer_class(data=job) - serializer.is_valid() - result = serializer.save() - - self.assertEquals(result.title, "job test") - self.assertIs(result.skills.count(), 0) - -from django.test import TestCase, Client, RequestFactory -from djangoldp.views import LDPViewSet -from djangoldp.permissions import AnonymousReadOnly - -from django.contrib.auth.models import AnonymousUser, User -from djangoldp_joboffer.models import JobOffer - - -class TestUserPermissions (TestCase): - def setUp(self): - self.factory = RequestFactory() -# self.c = Client() - self.user = User.objects.create_user(username='john', email='jlennon@beatles.com', password='glass onion') - - def tearDown(self): - self.user.delete() - - def test_get_with_user(self): - request = self.factory.get('/job-offers/') - request.user = self.user - my_view = LDPViewSet.as_view({'get': 'list'}, model=JobOffer, nested_fields=["skills"], permission_classes=[AnonymousReadOnly]) - response = my_view(request) - self.assertEqual(response.status_code, 200) - - def test_request_options_create_with_user(self): - request = self.factory.options('/job-offers/') - request.user = self.user - my_view = LDPViewSet.as_view({'options': 'create'}, model=JobOffer, nested_fields=["skills"], permission_classes=[AnonymousReadOnly]) - response = my_view(request) - self.assertEqual(response.status_code, 201) - - def test_request_options_update_with_user(self): - request = self.factory.options('/job-offers/') - request.user = self.user - my_view = LDPViewSet.as_view({'options': 'update'}, model=JobOffer, nested_fields=["skills"], permission_classes=[AnonymousReadOnly]) - response = my_view(request) - self.assertEqual(response.status_code, 201) - - -class TestAnonymousUserPermissions (TestCase): - def setUp(self): - self.factory = RequestFactory() -# self.c = Client() - self.user = AnonymousUser - - def test_get_request_with_anonymousUser(self): - request = self.factory.get("/job-offers/") - request.user = self.user - my_view = LDPViewSet.as_view({'get': 'list'}, - model=JobOffer, - nested_fields=["skills"], - permission_classes=[AnonymousReadOnly]) - response = my_view(request) - self.assertEqual(response.status_code, 200) - - def test_request_options_create_with_anonymousUser(self): - request = self.factory.options("/job-offers/") - request.user = self.user - my_view = LDPViewSet.as_view({'options': 'create'}, - model=JobOffer, - nested_fields=["skills"], - permission_classes=[AnonymousReadOnly]) - response = my_view(request) - self.assertEqual(response.status_code, 403) - - def test_request_options_update_with_anonymousUser(self): - request = self.factory.options("/job-offers/") - request.user = self.user - my_view = LDPViewSet.as_view({'options': 'update'}, - model=JobOffer, - nested_fields=["skills"], - permission_classes=[AnonymousReadOnly]) - response = my_view(request) - self.assertEqual(response.status_code, 403) - - diff --git a/djangoldp/tests/tests_anonymous_permissions.py b/djangoldp/tests/tests_anonymous_permissions.py new file mode 100644 index 0000000000000000000000000000000000000000..a8f4ee3f471f3d245c8f92fbd00ecfbd73d08fd8 --- /dev/null +++ b/djangoldp/tests/tests_anonymous_permissions.py @@ -0,0 +1,43 @@ +from django.contrib.auth.models import AnonymousUser +from django.test import TestCase, RequestFactory + +from djangoldp.permissions import AnonymousReadOnly +from djangoldp.tests.models import JobOffer +from djangoldp.views import LDPViewSet + + +class TestAnonymousUserPermissions(TestCase): + def setUp(self): + self.factory = RequestFactory() + # self.c = Client() + self.user = AnonymousUser + + def test_get_request_with_anonymousUser(self): + request = self.factory.get("/job-offers/") + request.user = self.user + my_view = LDPViewSet.as_view({'get': 'list'}, + model=JobOffer, + nested_fields=["skills"], + permission_classes=[AnonymousReadOnly]) + response = my_view(request) + self.assertEqual(response.status_code, 200) + + def test_request_options_create_with_anonymousUser(self): + request = self.factory.options("/job-offers/") + request.user = self.user + my_view = LDPViewSet.as_view({'options': 'create'}, + model=JobOffer, + nested_fields=["skills"], + permission_classes=[AnonymousReadOnly]) + response = my_view(request) + self.assertEqual(response.status_code, 403) + + def test_request_options_update_with_anonymousUser(self): + request = self.factory.options("/job-offers/") + request.user = self.user + my_view = LDPViewSet.as_view({'options': 'update'}, + model=JobOffer, + nested_fields=["skills"], + permission_classes=[AnonymousReadOnly]) + response = my_view(request) + self.assertEqual(response.status_code, 403) diff --git a/djangoldp/tests/tests_ldp_model.py b/djangoldp/tests/tests_ldp_model.py new file mode 100644 index 0000000000000000000000000000000000000000..6400378cee8383cd39bf1fc0ff30d5cc45204630 --- /dev/null +++ b/djangoldp/tests/tests_ldp_model.py @@ -0,0 +1,39 @@ +import unittest + +from django.test import TestCase + +from djangoldp.models import Model +from djangoldp.tests.models import Dummy, LDPDummy + + +class LDPModelTest(TestCase): + + def test_class_not_inheriting_ldp_model(self): + dummy = Dummy.objects.create(some="text") + self.assertEquals("/dummys/", Model.container_id(dummy)) + self.assertEquals("/dummys/{}".format(dummy.pk), Model.resource_id(dummy)) + + def test_class_inheriting_ldp_model(self): + dummy = LDPDummy.objects.create(some="text") + self.assertEquals("/ldpdummys/", dummy.get_container_id()) + self.assertEquals("/ldpdummys/{}".format(dummy.pk), dummy.get_absolute_url()) + self.assertEquals("/ldpdummys/", Model.container_id(dummy)) + self.assertEquals("/ldpdummys/{}".format(dummy.pk), Model.resource_id(dummy)) + + def test_from_resolve_id(self): + saved_instance = Dummy.objects.create(some="text") + result = Model.resolve_id("/dummys/{}".format(saved_instance.pk)) + self.assertEquals(saved_instance, result) + + def test_resolve_container(self): + result = Model.resolve_container("/dummys/") + self.assertEquals(Dummy, result) + + @unittest.skip("futur feature: avoid urls.py on apps") + def test_auto_url(self): + from django.urls import get_resolver + dummy = LDPDummy.objects.create(some="text") + view_name = '{}-list'.format(dummy._meta.object_name.lower()) + path = '/{}'.format(get_resolver().reverse_dict[view_name][0][0][0], dummy.pk) + + self.assertEquals(path, dummy.get_absolute_url()) diff --git a/djangoldp/tests/tests_save.py b/djangoldp/tests/tests_save.py new file mode 100644 index 0000000000000000000000000000000000000000..7e05496ab21ab8aff4e639bd6d80dcc5c8873ae5 --- /dev/null +++ b/djangoldp/tests/tests_save.py @@ -0,0 +1,49 @@ +from django.test import TestCase + +from djangoldp.serializers import LDPSerializer +from djangoldp.tests.models import Skill, JobOffer + + +class Save(TestCase): + + def test_save_m2m(self): + skill1 = Skill.objects.create(title="skill1", obligatoire="obligatoire") + skill2 = Skill.objects.create(title="skill2", obligatoire="obligatoire") + + job = {"title": "job test", + "skills": { + "ldp:contains": [ + {"@id": "https://happy-dev.fr/skills/{}/".format(skill1.pk)}, + {"@id": "https://happy-dev.fr/skills/{}/".format(skill2.pk), "title": "skill2 UP"}, + ]} + } + + meta_args = {'model': JobOffer, 'depth': 1, 'fields': ("@id", "title", "skills")} + + meta_class = type('Meta', (), meta_args) + serializer_class = type(LDPSerializer)('JobOfferSerializer', (LDPSerializer,), {'Meta': meta_class}) + serializer = serializer_class(data=job) + serializer.is_valid() + result = serializer.save() + + self.assertEquals(result.title, "job test") + self.assertIs(result.skills.count(), 2) + self.assertEquals(result.skills.all()[0].title, "skill1") # no change + self.assertEquals(result.skills.all()[1].title, "skill2 UP") # title updated + + def test_save_without_nested_fields(self): + skill1 = Skill.objects.create(title="skill1", obligatoire="obligatoire") + skill2 = Skill.objects.create(title="skill2", obligatoire="obligatoire") + job = {"title": "job test"} + + meta_args = {'model': JobOffer, 'depth': 1, 'fields': ("@id", "title", "skills")} + + meta_class = type('Meta', (), meta_args) + serializer_class = type(LDPSerializer)('JobOfferSerializer', (LDPSerializer,), {'Meta': meta_class}) + serializer = serializer_class(data=job) + serializer.is_valid() + result = serializer.save() + + self.assertEquals(result.title, "job test") + self.assertIs(result.skills.count(), 0) + diff --git a/djangoldp/tests/tests_update.py b/djangoldp/tests/tests_update.py new file mode 100644 index 0000000000000000000000000000000000000000..39083e2038e831663e52137251f5cd9fec1fdfce --- /dev/null +++ b/djangoldp/tests/tests_update.py @@ -0,0 +1,252 @@ +from django.contrib.auth.models import User +from django.test import TestCase + +from djangoldp.serializers import LDPSerializer +from djangoldp.tests.models import Skill, JobOffer, Thread, Message + + +class Update(TestCase): + + def test_update(self): + skill = Skill.objects.create(title="to drop", obligatoire="obligatoire") + skill1 = Skill.objects.create(title="skill1", obligatoire="obligatoire") + skill2 = Skill.objects.create(title="skill2", obligatoire="obligatoire") + job1 = JobOffer.objects.create(title="job test") + job1.skills.add(skill) + + job = {"@id": "https://happy-dev.fr/job-offers/{}/".format(job1.pk), + "title": "job test updated", + "skills": { + "ldp:contains": [ + {"title": "new skill", "obligatoire": "okay"}, + {"@id": "https://happy-dev.fr/skills/{}/".format(skill1.pk)}, + {"@id": "https://happy-dev.fr/skills/{}/".format(skill2.pk), "title": "skill2 UP"}, + ]} + } + + meta_args = {'model': JobOffer, 'depth': 1, 'fields': ("@id", "title", "skills")} + + meta_class = type('Meta', (), meta_args) + serializer_class = type(LDPSerializer)('JobOfferSerializer', (LDPSerializer,), {'Meta': meta_class}) + serializer = serializer_class(data=job, instance=job1) + serializer.is_valid() + result = serializer.save() + + self.assertEquals(result.title, "job test updated") + self.assertIs(result.skills.count(), 3) + skills = result.skills.all().order_by('title') + self.assertEquals(skills[0].title, "new skill") # new skill + self.assertEquals(skills[1].title, "skill1") # no change + self.assertEquals(skills[2].title, "skill2 UP") # title updated + + def test_update_graph(self): + skill = Skill.objects.create(title="to drop", obligatoire="obligatoire") + skill1 = Skill.objects.create(title="skill1", obligatoire="obligatoire") + skill2 = Skill.objects.create(title="skill2", obligatoire="obligatoire") + job1 = JobOffer.objects.create(title="job test") + job1.skills.add(skill) + + job = {"@graph": + [ + { + "@id": "https://happy-dev.fr/job-offers/{}/".format(job1.pk), + "title": "job test updated", + "skills": { + "ldp:contains": [ + {"@id": "https://happy-dev.fr/skills/{}/".format(skill1.pk)}, + {"@id": "https://happy-dev.fr/skills/{}/".format(skill2.pk)}, + {"@id": "_.123"}, + ]} + }, + { + "@id": "_.123", + "title": "new skill", + "obligatoire": "okay" + }, + { + "@id": "https://happy-dev.fr/skills/{}/".format(skill1.pk), + }, + { + "@id": "https://happy-dev.fr/skills/{}/".format(skill2.pk), + "title": "skill2 UP" + } + ] + } + + meta_args = {'model': JobOffer, 'depth': 1, 'fields': ("@id", "title", "skills")} + + meta_class = type('Meta', (), meta_args) + serializer_class = type(LDPSerializer)('JobOfferSerializer', (LDPSerializer,), {'Meta': meta_class}) + serializer = serializer_class(data=job, instance=job1) + serializer.is_valid() + result = serializer.save() + + skills = result.skills.all().order_by('title') + + self.assertEquals(result.title, "job test updated") + self.assertIs(result.skills.count(), 3) + self.assertEquals(skills[0].title, "new skill") # new skill + self.assertEquals(skills[1].title, "skill1") # no change + self.assertEquals(skills[2].title, "skill2 UP") # title updated + + def test_update_graph_2(self): + skill = Skill.objects.create(title="to drop", obligatoire="obligatoire") + skill1 = Skill.objects.create(title="skill1", obligatoire="obligatoire") + skill2 = Skill.objects.create(title="skill2", obligatoire="obligatoire") + job1 = JobOffer.objects.create(title="job test") + job1.skills.add(skill) + + job = {"@graph": + [ + { + "@id": "https://happy-dev.fr/job-offers/{}/".format(job1.pk), + "title": "job test updated", + "skills": { + "@id": "https://happy-dev.fr/job-offers/{}/skills/".format(job1.pk) + } + }, + { + "@id": "_.123", + "title": "new skill", + "obligatoire": "okay" + }, + { + "@id": "https://happy-dev.fr/skills/{}/".format(skill1.pk), + }, + { + "@id": "https://happy-dev.fr/skills/{}/".format(skill2.pk), + "title": "skill2 UP" + }, + { + '@id': "https://happy-dev.fr/job-offers/{}/skills/".format(job1.pk), + "ldp:contains": [ + {"@id": "https://happy-dev.fr/skills/{}/".format(skill1.pk)}, + {"@id": "https://happy-dev.fr/skills/{}/".format(skill2.pk)}, + {"@id": "_.123"}, + ] + } + ] + } + + meta_args = {'model': JobOffer, 'depth': 1, 'fields': ("@id", "title", "skills")} + + meta_class = type('Meta', (), meta_args) + serializer_class = type(LDPSerializer)('JobOfferSerializer', (LDPSerializer,), {'Meta': meta_class}) + serializer = serializer_class(data=job, instance=job1) + serializer.is_valid() + result = serializer.save() + + skills = result.skills.all().order_by('title') + + self.assertEquals(result.title, "job test updated") + self.assertIs(result.skills.count(), 3) + self.assertEquals(skills[0].title, "new skill") # new skill + self.assertEquals(skills[1].title, "skill1") # no change + self.assertEquals(skills[2].title, "skill2 UP") # title updated + self.assertEquals(skill, skill._meta.model.objects.get(pk=skill.pk)) # title updated + + def test_update_list_with_reverse_relation(self): + user1 = User.objects.create() + thread = Thread.objects.create(description="Thread 1", author_user=user1) + message1 = Message.objects.create(text="Message 1", thread=thread, author_user=user1) + message2 = Message.objects.create(text="Message 2", thread=thread, author_user=user1) + + json = {"@graph": [ + { + "@id": "https://happy-dev.fr/messages/{}/".format(message1.pk), + "text": "Message 1 UP" + }, + { + "@id": "https://happy-dev.fr/messages/{}/".format(message2.pk), + "text": "Message 2 UP" + }, + { + '@id': "https://happy-dev.fr/threads/{}/".format(thread.pk), + 'description': "Thread 1 UP", + "message_set": [ + {"@id": "https://happy-dev.fr/messages/{}/".format(message1.pk)}, + {"@id": "https://happy-dev.fr/messages/{}/".format(message2.pk)}, + ] + } + ] + } + + meta_args = {'model': Thread, 'depth': 1, 'fields': ("@id", "description", "message_set")} + + meta_class = type('Meta', (), meta_args) + serializer_class = type(LDPSerializer)('ThreadSerializer', (LDPSerializer,), {'Meta': meta_class}) + serializer = serializer_class(data=json, instance=thread) + serializer.is_valid() + result = serializer.save() + + messages = result.message_set.all().order_by('text') + + self.assertEquals(result.description, "Thread 1 UP") + self.assertIs(result.message_set.count(), 2) + self.assertEquals(messages[0].text, "Message 1 UP") + self.assertEquals(messages[1].text, "Message 2 UP") + + def test_add_new_element_with_foreign_key_id(self): + user1 = User.objects.create() + thread = Thread.objects.create(description="Thread 1", author_user=user1) + message1 = Message.objects.create(text="Message 1", thread=thread, author_user=user1) + message2 = Message.objects.create(text="Message 2", thread=thread, author_user=user1) + + json = {"@graph": [ + { + "@id": "https://happy-dev.fr/messages/{}/".format(message1.pk), + "text": "Message 1 UP", + "author_user": { + '@id': "https://happy-dev.fr/users/{}/".format(user1.pk) + } + }, + { + "@id": "https://happy-dev.fr/messages/{}/".format(message2.pk), + "text": "Message 2 UP", + "author_user": { + '@id': "https://happy-dev.fr/users/{}/".format(user1.pk) + } + }, + { + "@id": "_:b1", + "text": "Message 3 NEW", + "author_user": { + '@id': "https://happy-dev.fr/users/{}/".format(user1.pk) + } + }, + { + '@id': "https://happy-dev.fr/threads/{}".format(thread.pk), + "author_user": { + '@id': "https://happy-dev.fr/users/{}/".format(user1.pk) + }, + 'description': "Thread 1 UP", + 'message_set': { + "@id": "https://happy-dev.fr/threads/{}/message_set".format(thread.pk) + } + }, + { + '@id': "https://happy-dev.fr/threads/{}/message_set".format(thread.pk), + "ldp:contains": [ + {"@id": "https://happy-dev.fr/messages/{}/".format(message1.pk)}, + {"@id": "https://happy-dev.fr/messages/{}/".format(message2.pk)}, + {"@id": "_:b1"} + ] + } + ] + } + + meta_args = {'model': Thread, 'depth': 1, 'fields': ("@id", "description", "message_set")} + + meta_class = type('Meta', (), meta_args) + serializer_class = type(LDPSerializer)('ThreadSerializer', (LDPSerializer,), {'Meta': meta_class}) + serializer = serializer_class(data=json, instance=thread) + serializer.is_valid() + result = serializer.save() + + messages = result.message_set.all().order_by('text') + + self.assertEquals(result.description, "Thread 1 UP") + self.assertIs(result.message_set.count(), 3) + self.assertEquals(messages[0].text, "Message 1 UP") + self.assertEquals(messages[1].text, "Message 2 UP") + self.assertEquals(messages[2].text, "Message 3 NEW") diff --git a/djangoldp/tests/tests_user_permissions.py b/djangoldp/tests/tests_user_permissions.py new file mode 100644 index 0000000000000000000000000000000000000000..3ad30ddd3af62424ec42db1e544fe425ef32f9db --- /dev/null +++ b/djangoldp/tests/tests_user_permissions.py @@ -0,0 +1,40 @@ +from django.contrib.auth.models import User +from django.test import TestCase, RequestFactory + +from djangoldp.permissions import AnonymousReadOnly +from djangoldp.tests.models import JobOffer +from djangoldp.views import LDPViewSet + + +class TestUserPermissions(TestCase): + def setUp(self): + self.factory = RequestFactory() + # self.c = Client() + self.user = User.objects.create_user(username='john', email='jlennon@beatles.com', password='glass onion') + + def tearDown(self): + self.user.delete() + + def test_get_with_user(self): + request = self.factory.get('/job-offers/') + request.user = self.user + my_view = LDPViewSet.as_view({'get': 'list'}, model=JobOffer, nested_fields=["skills"], + permission_classes=[AnonymousReadOnly]) + response = my_view(request) + self.assertEqual(response.status_code, 200) + + def test_request_options_create_with_user(self): + request = self.factory.options('/job-offers/') + request.user = self.user + my_view = LDPViewSet.as_view({'options': 'create'}, model=JobOffer, nested_fields=["skills"], + permission_classes=[AnonymousReadOnly]) + response = my_view(request) + self.assertEqual(response.status_code, 201) + + def test_request_options_update_with_user(self): + request = self.factory.options('/job-offers/') + request.user = self.user + my_view = LDPViewSet.as_view({'options': 'update'}, model=JobOffer, nested_fields=["skills"], + permission_classes=[AnonymousReadOnly]) + response = my_view(request) + self.assertEqual(response.status_code, 201) diff --git a/djangoldp/tests/urls.py b/djangoldp/tests/urls.py index 5982784daeaeaf8c7b1f8e59f784632b7ae95cbe..392b0e2b7764a2fe37fa87e1b47f3ea9916f66f7 100644 --- a/djangoldp/tests/urls.py +++ b/djangoldp/tests/urls.py @@ -1,4 +1,6 @@ -from djangoldp.tests.models import Skill, JobOffer +from django.conf import settings + +from djangoldp.tests.models import Skill, JobOffer, Message, Thread, Dummy, LDPDummy from djangoldp.views import LDPViewSet from django.conf.urls import url @@ -6,4 +8,9 @@ from django.conf.urls import url urlpatterns = [ url(r'^skills/', LDPViewSet.urls(model=Skill, permission_classes=[], fields=["@id", "title"], nested_fields=[])), url(r'^job-offers/', LDPViewSet.urls(model=JobOffer, nested_fields=["skills"], permission_classes=())), + url(r'^messages/', LDPViewSet.urls(model=Message, permission_classes=[], fields=["@id", "text"], nested_fields=[])), + url(r'^threads/', LDPViewSet.urls(model=Thread, nested_fields=["message_set"], permission_classes=())), + url(r'^users/', LDPViewSet.urls(model=settings.AUTH_USER_MODEL, permission_classes=[])), + url(r'^dummys/', LDPViewSet.urls(model=Dummy, permission_classes=[])), + url(r'^ldp-dummys/', LDPViewSet.urls(model=LDPDummy, permission_classes=[])), ] \ No newline at end of file diff --git a/djangoldp/views.py b/djangoldp/views.py index 9f38b07954063deaab01ec59ac33e37f3c306a17..f4b2fb59f72c56b898174c6329c7b8f8ee606393 100644 --- a/djangoldp/views.py +++ b/djangoldp/views.py @@ -89,9 +89,10 @@ class LDPViewSet(LDPViewSetGenerator): """An automatically generated viewset that serves models following the Linked Data Platform convention""" fields = None exclude = None - depth = 0 + depth = 1 + many_depth = 0 renderer_classes = (JSONLDRenderer,) - parser_classes =(JSONLDParser,) + parser_classes = (JSONLDParser,) authentication_classes = (NoCSRFAuthentication,) def __init__(self, **kwargs): @@ -106,7 +107,9 @@ class LDPViewSet(LDPViewSetGenerator): def build_serializer(self): model_name = self.model._meta.object_name.lower() lookup_field = get_resolver().reverse_dict[model_name + '-detail'][0][0][1][0] - meta_args = {'model': self.model, 'extra_kwargs': {'@id': {'lookup_field': lookup_field}}, 'depth': self.depth, + meta_args = {'model': self.model, 'extra_kwargs': { + '@id': {'lookup_field': lookup_field}}, + 'depth': self.depth, 'extra_fields': self.nested_fields} if self.fields: meta_args['fields'] = self.fields @@ -135,12 +138,16 @@ class LDPViewSet(LDPViewSetGenerator): response["Access-Control-Allow-Headers"] = "Content-Type, if-match" response["Access-Control-Allow-Credentials"] = 'true' response["Accept-Post"] = "application/ld+json" + if response.status_code == 201 and '@id' in response.data: + response["Location"] = response.data['@id'] + response["Accept-Post"] = "application/ld+json" return response def update(self, request, *args, **kwargs): response = super().update(request, *args, **kwargs) return response + class LDPNestedViewSet(LDPViewSet): """A special case of LDPViewSet serving objects of a relation of a given object (e.g. members of a group, or skills of a user)""" parent_model = None