Skip to content
Snippets Groups Projects
Commit 7b7c37dd authored by Calum Mackervoy's avatar Calum Mackervoy
Browse files

Merge branch 'master' into 196-backlinks-support

parents e997443e ed831485
No related branches found
No related tags found
No related merge requests found
......@@ -19,10 +19,11 @@ test:
publish:
stage: release
before_script:
- pip install python-semantic-release sib-commit-parser
- pip install python-semantic-release~=5.0 sib-commit-parser~=0.3
- git config user.name "${GITLAB_USER_NAME}"
- git config user.email "${GITLAB_USER_EMAIL}"
- git remote set-url origin "https://gitlab-ci-token:${GL_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git"
- git fetch --tags
script:
- semantic-release publish
only:
......
......@@ -182,14 +182,31 @@ class Todo(Model):
class Meta(Model.Meta):
```
To enable federation, meaning that local users can access objects from another server as if they were on the local server, DjangoLDP creates backlinks, local copies of the object containing the URL-id (@id) of the distant resource. This is a key concept in LDP. To read more, see the [W3C primer on LDP](https://www.w3.org/TR/ldp-primer/), and the [LDP specification](https://www.w3.org/TR/ldp/)
For situations where you don't want to include federated resources in a queryset, DjangoLDP Models override `models.Manager`, allowing you to write `Todo.objects.local()`, for example:
```python
Todo.objects.create(name='Local Todo')
Todo.objects.create(name='Distant Todo', urlid='https://anotherserversomewhere.com/todos/1/')
Todo.objects.all() # query set containing { Local Todo, Distant Todo }
Todo.objects.local() # { Local Todo } only
```
For Views, we also define a FilterBackend to achieve the same purpose. See the section on ViewSets for this purpose
See "Custom Meta options" below to see some helpful ways you can tweak the behaviour of DjangoLDP
Your model will be automatically detected and registered with an LDPViewSet and corresponding URLs, as well as being registered with the Django admin panel. If you register your model with the admin panel manually, make sure to extend the GuardedModelAdmin so that the model is registered with [Django-Guardian object permissions](https://django-guardian.readthedocs.io/en/stable/userguide/admin-integration.html)
## Custom Parameters to LDPViewSet
## LDPViewSet
DjangoLDP automatically generates ViewSets for your models, and registers these at urls, according to the settings configured in the model Meta (see below for options)
### lookup_field
### Custom Parameters
#### lookup_field
Can be used to use a slug in the url instead of the primary key.
......@@ -197,7 +214,7 @@ Can be used to use a slug in the url instead of the primary key.
LDPViewSet.urls(model=User, lookup_field='username')
```
### nested_fields
#### nested_fields
list of ForeignKey, ManyToManyField, OneToOneField and their reverse relations. When a field is listed in this parameter, a container will be created inside each single element of the container.
......@@ -207,6 +224,37 @@ In the following example, besides the urls `/members/` and `/members/<pk>/`, two
<Model>._meta.nested_fields=["skills"]
```
## Filter Backends
To achieve federation, DjangoLDP includes links to objects from federated servers and stores these as local objects (see 1.0 - Models). In some situations, you will want to exclude these from the queryset of a custom view
To provide for this need, there is defined in `djangoldp.filters` a FilterBackend which can be included in custom viewsets to restrict the queryset to only objects which were created locally:
```python
from djangoldp.filters import LocalObjectFilterBackend
class MyViewSet(..):
filter_backends=[LocalObjectFilterBackend]
```
By default, LDPViewset applies filter backends from the `permission_classes` defined on the model (see 3.1 for configuration)
By default, `LDPViewSets` use another FilterBackend, `LocalObjectOnContainerPathBackend`, which ensures that only local objects are returned when the path matches that of the Models `container_path` (e.g. /users/ will return a list of local users). In very rare situations where this might be undesirable, it's possible to extend `LDPViewSet` and remove the filter_backend:
```python
class LDPSourceViewSet(LDPViewSet):
model = LDPSource
filter_backends = []
```
Following this you will need to update the model's Meta to use the custom `view_set`:
```python
class Meta:
view_set = LDPSourceViewSet
```
## Custom Meta options on models
### rdf_type
......
from rest_framework.filters import BaseFilterBackend
from djangoldp.models import Model
class LocalObjectFilterBackend(BaseFilterBackend):
"""
Filter which removes external objects (federated backlinks) from the queryset
For querysets which should only include local objects
"""
def filter_queryset(self, request, queryset, view):
internal_ids = [x.pk for x in queryset if not Model.is_external(x)]
return queryset.filter(pk__in=internal_ids)
class LocalObjectOnContainerPathBackend(LocalObjectFilterBackend):
"""
Override of LocalObjectFilterBackend which removes external objects when the view requested
is the model container path
"""
def filter_queryset(self, request, queryset, view):
if issubclass(view.model, Model) and request.path_info == view.model.get_container_path():
return super(LocalObjectOnContainerPathBackend, self).filter_queryset(request, queryset, view)
return queryset
......@@ -13,8 +13,17 @@ from djangoldp.fields import LDPUrlField
from djangoldp.permissions import LDPPermissions
class LDPModelManager(models.Manager):
# an alternative to all() which exlcudes external resources
def local(self):
queryset = super(LDPModelManager, self).all()
internal_ids = [x.pk for x in queryset if not Model.is_external(x)]
return queryset.filter(pk__in=internal_ids)
class Model(models.Model):
urlid = LDPUrlField(blank=True, null=True, unique=True)
objects = LDPModelManager()
def __init__(self, *args, **kwargs):
super(Model, self).__init__(*args, **kwargs)
......
......@@ -23,9 +23,12 @@ class TestGET(APITestCase):
def test_get_container(self):
Post.objects.create(content="content")
# federated object - should not be returned in the container view
Post.objects.create(content="federated", urlid="https://external.com/posts/1/")
response = self.client.get('/posts/', content_type='application/ld+json')
self.assertEqual(response.status_code, 200)
self.assertIn('permissions', response.data)
self.assertEquals(1, len(response.data['ldp:contains']))
self.assertEquals(2, len(response.data['permissions'])) # read and add
Invoice.objects.create(title="content")
......@@ -38,6 +41,7 @@ class TestGET(APITestCase):
Post.objects.all().delete()
response = self.client.get('/posts/', content_type='application/ld+json')
self.assertEqual(response.status_code, 200)
self.assertEquals(0, len(response.data['ldp:contains']))
def test_get_filtered_fields(self):
skill = Skill.objects.create(title="Java", obligatoire="ok", slug="1")
......@@ -79,6 +83,8 @@ class TestGET(APITestCase):
def test_get_nested(self):
invoice = Invoice.objects.create(title="invoice")
batch = Batch.objects.create(invoice=invoice, title="batch")
distant_batch = Batch.objects.create(invoice=invoice, title="distant", urlid="https://external.com/batch/1/")
response = self.client.get('/invoices/{}/batches/'.format(invoice.pk), content_type='application/ld+json')
self.assertEqual(response.status_code, 200)
self.assertEquals(response.data['@id'], 'http://happy-dev.fr/invoices/{}/batches/'.format(invoice.pk))
self.assertEquals(len(response.data['ldp:contains']), 2)
......@@ -17,8 +17,10 @@ class TestSource(APITestCase):
response = self.client.get('/sources/{}/'.format(source.federation), content_type='application/ld+json')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['@id'], 'http://happy-dev.fr/sources/source_name/')
self.assertEqual(len(response.data['ldp:contains']), 1)
def test_get_empty_resource(self):
response = self.client.get('/sources/{}/'.format('unknown'), content_type='application/ld+json')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['@id'], 'http://happy-dev.fr/sources/unknown/')
self.assertEqual(len(response.data['ldp:contains']), 0)
......@@ -20,6 +20,7 @@ from rest_framework.viewsets import ModelViewSet
from djangoldp.endpoints.webfinger import WebFingerEndpoint, WebFingerError
from djangoldp.models import LDPSource, Model
from djangoldp.permissions import LDPPermissions
from djangoldp.filters import LocalObjectOnContainerPathBackend
get_user_model()._meta.rdf_context = {"get_full_name": "rdfs:label"}
......@@ -133,6 +134,7 @@ class LDPViewSet(LDPViewSetGenerator):
renderer_classes = (JSONLDRenderer,)
parser_classes = (JSONLDParser,)
authentication_classes = (NoCSRFAuthentication,)
filter_backends = [LocalObjectOnContainerPathBackend]
def __init__(self, **kwargs):
super().__init__(**kwargs)
......@@ -141,7 +143,7 @@ class LDPViewSet(LDPViewSetGenerator):
if self.permission_classes:
for p in self.permission_classes:
if hasattr(p, 'filter_class') and p.filter_class:
self.filter_backends = p.filter_class
self.filter_backends.append(p.filter_class)
self.serializer_class = self.build_read_serializer()
self.write_serializer_class = self.build_write_serializer()
......@@ -320,6 +322,7 @@ class LDPNestedViewSet(LDPViewSet):
class LDPSourceViewSet(LDPViewSet):
model = LDPSource
federation = None
filter_backends = []
def get_queryset(self, *args, **kwargs):
return super().get_queryset(*args, **kwargs).filter(federation=self.kwargs['federation'])
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment