Repository: Ian-Foote/rest-framework-generic-relations
Branch: master
Commit: bdc028ca9457
Files: 19
Total size: 40.4 KB
Directory structure:
gitextract_9csql789/
├── .github/
│ └── workflows/
│ └── tests.yml
├── .gitignore
├── Changelog.md
├── Makefile
├── README.md
├── generic_relations/
│ ├── __init__.py
│ ├── relations.py
│ ├── serializers.py
│ └── tests/
│ ├── __init__.py
│ ├── migrations/
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ ├── models.py
│ ├── test_relations.py
│ └── test_serializers.py
├── manage.py
├── setup.cfg
├── setup.py
├── testsettings.py
└── tox.ini
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/tests.yml
================================================
name: tests
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version:
- "3.7"
- "3.11"
django-version:
- "2.2"
- "3.1"
- "3.2"
- "4.1"
drf-version:
- "3.11"
- "3.12"
- "3.13"
- "3.14"
exclude:
- django-version: "2.2"
drf-version: "3.13"
- django-version: "3.1"
drf-version: "3.13"
- django-version: "2.2"
drf-version: "3.14"
- django-version: "3.1"
drf-version: "3.14"
- django-version: "4.1"
drf-version: "3.11"
- django-version: "4.1"
drf-version: "3.12"
- django-version: "4.1"
python-version: "3.7"
name: "py${{ matrix.python-version}} dj${{ matrix.django-version }} drf${{ matrix.drf-version }}"
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies (django ${{ matrix.django-version }})
run: |
python -m pip install --upgrade pip
pip install flake8 pytest-django django==${{ matrix.django-version }}.* djangorestframework==${{ matrix.drf-version }}.*
pip install -e .
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
- name: Test with pytest
run: |
pytest --ds=testsettings generic_relations/tests/
================================================
FILE: .gitignore
================================================
*.pyc
__pycache__
MANIFEST
dist/
build/
*.egg-info/
.eggs
.tox
================================================
FILE: Changelog.md
================================================
# Rest Framework Generic Relations Changelog
## v2.1.0
General dependency update
* Minimum Python version is now 3.6
* Minimum DRF version is now 3.11
* Supported Django versions are now [2.2, 3.1, 3.2]
## v2.0.0
* Add Python 3.8, Django 3.0 and DRF 3.11 support
* Drop Python 2
* Drop DRF 3.7
Minimum supported dependencies are now:
* Python 3.4
* Django 1.11
* DRF 3.8
## v1.2.1
* Add error handling for reusing a `Serializer` instance in a `GenericRelatedField`.
## v1.2.0
* Add Django 2.0 support (need Python min 3.4, Django min. 1.11).
## v1.1.0
* Add `GenericModelSerializer` as a counterpart to `GenericRelatedField`.
* Dynamically determine the best serializer to use to serialize a model instance.
* Rename `determine_serializer_for_data` to `get_deserializer_for_data`
* Rename `determine_deserializer_for_data` to `get_serializer_for_instance`
## v1.0.0
* Add support for Django 1.8 and 1.9
* Add support for Django Rest Framework 3
* Drop support for earlier versions of Django and DRF
## v0.1.0
* Initial release - Forked from https://github.com/encode/django-rest-framework/pull/755
================================================
FILE: Makefile
================================================
SHELL := /bin/bash
help:
@echo "Usage:"
@echo " make help -- displays this help"
@echo " make test -- runs tests"
@echo " make release -- pushes to pypi"
test:
tox
release:
rm -rf dist
python setup.py sdist bdist_wheel
twine upload dist/*
================================================
FILE: README.md
================================================
# Rest Framework Generic Relations [](https://github.com/Ian-Foote/rest-framework-generic-relations/actions/workflows/tests.yml)
This library implements [Django REST Framework](http://www.django-rest-framework.org/) serializers to handle generic foreign keys.
# Requirements
Any currently-supported combination of Django REST Framework, Python, and Django.
# Installation
Install using `pip`...
```sh
pip install rest-framework-generic-relations
```
Add `'generic_relations'` to your `INSTALLED_APPS` setting.
```python
INSTALLED_APPS = (
...
'generic_relations',
)
```
# API Reference
## GenericRelatedField
This field serializes generic foreign keys. For a primer on generic foreign keys, first see: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/
Let's assume a `TaggedItem` model which has a generic relationship with other arbitrary models:
```python
class TaggedItem(models.Model):
tag_name = models.SlugField()
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
tagged_object = GenericForeignKey('content_type', 'object_id')
```
And the following two models, which may have associated tags:
```python
class Bookmark(models.Model):
"""
A bookmark consists of a URL, and 0 or more descriptive tags.
"""
url = models.URLField()
tags = GenericRelation(TaggedItem)
class Note(models.Model):
"""
A note consists of some text, and 0 or more descriptive tags.
"""
text = models.CharField(max_length=1000)
tags = GenericRelation(TaggedItem)
```
Now we define serializers for each model that may get associated with tags.
```python
class BookmarkSerializer(serializers.ModelSerializer):
class Meta:
model = Bookmark
fields = ('url',)
class NoteSerializer(serializers.ModelSerializer):
class Meta:
model = Note
fields = ('text',)
```
The model serializer for the `TaggedItem` model could look like this:
```python
from generic_relations.relations import GenericRelatedField
class TagSerializer(serializers.ModelSerializer):
"""
A `TaggedItem` serializer with a `GenericRelatedField` mapping all possible
models to their respective serializers.
"""
tagged_object = GenericRelatedField({
Bookmark: BookmarkSerializer(),
Note: NoteSerializer()
})
class Meta:
model = TaggedItem
fields = ('tag_name', 'tagged_object')
```
The JSON representation of a `TaggedItem` object with `name='django'` and its generic foreign key pointing at a `Bookmark` object with `url='https://www.djangoproject.com/'` would look like this:
```json
{
"tagged_object": {
"url": "https://www.djangoproject.com/"
},
"tag_name": "django"
}
```
If you want to have your generic foreign key represented as hyperlink, simply use `HyperlinkedRelatedField` objects:
```python
class TagSerializer(serializers.ModelSerializer):
"""
A `Tag` serializer with a `GenericRelatedField` mapping all possible
models to properly set up `HyperlinkedRelatedField`s.
"""
tagged_object = GenericRelatedField({
Bookmark: serializers.HyperlinkedRelatedField(
queryset = Bookmark.objects.all(),
view_name='bookmark-detail',
),
Note: serializers.HyperlinkedRelatedField(
queryset = Note.objects.all(),
view_name='note-detail',
),
})
class Meta:
model = TaggedItem
fields = ('tag_name', 'tagged_object')
```
The JSON representation of the same `TaggedItem` example object could now look something like this:
```json
{
"tagged_object": "/bookmark/1/",
"tag_name": "django"
}
```
## Writing to generic foreign keys
The above `TagSerializer` is also writable. By default, a `GenericRelatedField` iterates over its nested serializers and returns the value of the first serializer that is actually able to perform `to_internal_value()` without any errors.
Note, that (at the moment) only `HyperlinkedRelatedField` is able to serialize model objects out of the box.
The following operations would create a `TaggedItem` object with it's `tagged_object` property pointing at the `Bookmark` object found at the given detail end point.
```python
tag_serializer = TagSerializer(data={
'tag_name': 'python',
'tagged_object': '/bookmark/1/'
})
tag_serializer.is_valid()
tag_serializer.save()
```
If you feel that this default behavior doesn't suit your needs, you can subclass `GenericRelatedField` and override its `get_serializer_for_instance` or `get_deserializer_for_data` respectively to implement your own way of decision-making.
## GenericModelSerializer
Sometimes you may want to serialize a single list of different top-level things. For instance, suppose I have an API view that returns what items are on my bookshelf. Let's define some models:
```python
from django.core.validators import MaxValueValidator
class Book(models.Model):
title = models.CharField(max_length=255)
author = models.CharField(max_length=255)
class Bluray(models.Model):
title = models.CharField(max_length=255)
rating = models.PositiveSmallIntegerField(
validators=[MaxValueValidator(5)],
)
```
Then we could have a serializer for each type of object:
```python
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ('title', 'author')
class BluraySerializer(serializers.ModelSerializer):
class Meta:
model = Bluray
fields = ('title', 'rating')
```
Now we can create a generic list serializer, which delegates to the above serializers based on the type of model it's serializing:
```python
bookshelf_item_serializer = GenericModelSerializer(
{
Book: BookSerializer(),
Bluray: BluraySerializer(),
},
many=True,
)
```
Then we can serialize a mixed list of items:
```python
>>> bookshelf_item_serializer.to_representation([
Book.objects.get(title='War and Peace'),
Bluray.objects.get(title='Die Hard'),
Bluray.objects.get(title='Shawshank Redemption'),
Book.objects.get(title='To Kill a Mockingbird'),
])
[
{'title': 'War and Peace', 'author': 'Leo Tolstoy'},
{'title': 'Die Hard', 'rating': 5},
{'title': 'Shawshank Redemption', 'rating': 5},
{'title': 'To Kill a Mockingbird', 'author': 'Harper Lee'}
]
```
## A few things you should note:
* Although `GenericForeignKey` fields can be set to any model object, the `GenericRelatedField` only handles models explicitly defined in its configuration dictionary.
* Reverse generic keys, expressed using the `GenericRelation` field, can be serialized using the regular relational field types, since the type of the target in the relationship is always known.
* The order in which you register serializers matters as far as write operations are concerned.
* Unless you provide a custom `get_deserializer_for_data()` method, only `HyperlinkedRelatedField` provides write access to generic model relations.
================================================
FILE: generic_relations/__init__.py
================================================
pkg_resources = __import__('pkg_resources')
distribution = pkg_resources.get_distribution('rest-framework-generic-relations')
__version__ = distribution.version
================================================
FILE: generic_relations/relations.py
================================================
from django.utils.deprecation import RenameMethodsBase
from rest_framework import serializers
from .serializers import GenericSerializerMixin
__all__ = ('GenericRelatedField',)
class RenamedMethods(RenameMethodsBase):
renamed_methods = (
('determine_deserializer_for_data', 'get_serializer_for_instance', DeprecationWarning),
('determine_serializer_for_data', 'get_deserializer_for_data', DeprecationWarning),
)
class GenericRelatedField(GenericSerializerMixin, serializers.Field, metaclass=RenamedMethods):
"""
Represents a generic relation / foreign key.
It's actually more of a wrapper, that delegates the logic to registered
serializers based on the `Model` class.
"""
================================================
FILE: generic_relations/serializers.py
================================================
from django.core.exceptions import ImproperlyConfigured
from django.utils.translation import gettext_lazy as _
from django import forms
from rest_framework import serializers
from rest_framework.settings import api_settings
__all__ = ('GenericSerializerMixin', 'GenericModelSerializer',)
class GenericSerializerMixin(object):
default_error_messages = {
'no_model_match': _('Invalid model - model not available.'),
'no_url_match': _('Invalid hyperlink - No URL match'),
'incorrect_url_match': _(
'Invalid hyperlink - view name not available'),
}
form_field_class = forms.URLField
def __init__(self, serializers, *args, **kwargs):
"""
Needs an extra parameter `serializers` which has to be a dict
key: value being `Model`: serializer.
"""
super(GenericSerializerMixin, self).__init__(*args, **kwargs)
self.serializers = serializers
for serializer in self.serializers.values():
if serializer.source is not None:
msg = '{}() cannot be re-used. Create a new instance.'
raise RuntimeError(msg.format(type(serializer).__name__))
serializer.bind('', self)
def to_internal_value(self, data):
try:
serializer = self.get_deserializer_for_data(data)
except ImproperlyConfigured as e:
raise serializers.ValidationError({api_settings.NON_FIELD_ERRORS_KEY: e})
return serializer.to_internal_value(data)
def to_representation(self, instance):
serializer = self.get_serializer_for_instance(instance)
return serializer.to_representation(instance)
def get_serializer_for_instance(self, instance):
# Use registered superclasses, rather than only the exact model.
# (But prefer things earlier in the MRO, so if the exact model is registered,
# use that in preference to any superclasses)
for klass in instance.__class__.mro():
if klass in self.serializers:
return self.serializers[klass]
raise serializers.ValidationError(self.error_messages['no_model_match'])
def get_deserializer_for_data(self, value):
# While one could easily execute the "try" block within
# to_internal_value and reduce operations, I consider the concept of
# serializing is already very naive and vague, that's why I'd
# go for stringency with the deserialization process here.
serializers = []
for serializer in self.serializers.values():
try:
serializer.to_internal_value(value)
# Collects all serializers that can handle the input data.
serializers.append(serializer)
except Exception:
pass
# If no serializer found, raise error.
l = len(serializers)
if l < 1:
raise ImproperlyConfigured(
'Could not determine a valid serializer for value %r.' % value)
elif l > 1:
raise ImproperlyConfigured(
'There were multiple serializers found for value %r.' % value)
return serializers[0]
class GenericModelSerializer(GenericSerializerMixin, serializers.Serializer):
"""
Delegates serialization and deserialization to registered serializers
based on the type of the model.
"""
================================================
FILE: generic_relations/tests/__init__.py
================================================
================================================
FILE: generic_relations/tests/migrations/0001_initial.py
================================================
# -*- coding: utf-8 -*-
# Generated by Django 1.11.22 on 2019-08-01 04:03
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
]
operations = [
migrations.CreateModel(
name='Bookmark',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('url', models.URLField()),
],
),
migrations.CreateModel(
name='Detachable',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
('object_id', models.PositiveIntegerField(blank=True, null=True)),
('content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
],
),
migrations.CreateModel(
name='Note',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('text', models.TextField()),
],
),
migrations.CreateModel(
name='Tag',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('tag', models.SlugField()),
('object_id', models.PositiveIntegerField()),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
],
),
migrations.CreateModel(
name='NoteProxy',
fields=[
],
options={
'proxy': True,
'indexes': [],
},
bases=('tests.note',),
),
]
================================================
FILE: generic_relations/tests/migrations/__init__.py
================================================
================================================
FILE: generic_relations/tests/models.py
================================================
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.db import models
class Tag(models.Model):
"""
Tags have a descriptive slug, and are attached to an arbitrary object.
"""
tag = models.SlugField()
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
tagged_item = GenericForeignKey('content_type', 'object_id')
def __unicode__(self):
return self.tag
class Detachable(models.Model):
"""
Model with an optional GenericForeignKey relation
"""
name = models.CharField(max_length=50)
content_type = models.ForeignKey(
ContentType, null=True, blank=True, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField(null=True, blank=True)
content_object = GenericForeignKey('content_type', 'object_id')
class Bookmark(models.Model):
"""
A URL bookmark that may have multiple tags attached.
"""
url = models.URLField()
tags = GenericRelation(Tag)
def __unicode__(self):
return 'Bookmark: %s' % self.url
class Note(models.Model):
"""
A textual note that may have multiple tags attached.
"""
text = models.TextField()
tags = GenericRelation(Tag)
def __unicode__(self):
return 'Note: %s' % self.text
class NoteProxy(Note):
class Meta:
proxy = True
================================================
FILE: generic_relations/tests/test_relations.py
================================================
import warnings
try:
from django.urls import re_path as url
except ImportError:
from django.conf.urls import url
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase, RequestFactory
from django.test.utils import override_settings
from rest_framework import serializers
from rest_framework.reverse import reverse
from rest_framework.settings import api_settings
from generic_relations.relations import GenericRelatedField
from generic_relations.tests.models import Bookmark, Detachable, Note, NoteProxy, Tag
warnings.simplefilter("default", DeprecationWarning)
factory = RequestFactory()
# Just to ensure we have a request in the serializer context
request = factory.get('/')
def dummy_view(request, pk):
pass
urlpatterns = [
url(r'^bookmark/(?P<pk>[0-9]+)/$', dummy_view, name='bookmark-detail'),
url(r'^detachable/(?P<pk>[0-9]+)/$', dummy_view, name='detachable-detail'),
url(r'^note/(?P<pk>[0-9]+)/$', dummy_view, name='note-detail'),
url(r'^tag/(?P<pk>[0-9]+)/$', dummy_view, name='tag-detail'),
url(
r'^contact/(?P<my_own_slug>[-\w]+)/$',
dummy_view,
name='contact-detail'
),
]
class BookmarkSerializer(serializers.ModelSerializer):
class Meta:
model = Bookmark
exclude = ('id', )
class NoteSerializer(serializers.ModelSerializer):
class Meta:
model = Note
exclude = ('id', )
class NoteProxySerializer(serializers.ModelSerializer):
text = serializers.SerializerMethodField()
class Meta:
model = NoteProxy
exclude = ('id',)
def get_text(self, instance):
return 'proxied: %s' % instance.text
@override_settings(ROOT_URLCONF='generic_relations.tests.test_relations')
class TestGenericRelatedFieldSerialization(TestCase):
def setUp(self):
self.bookmark = Bookmark.objects.create(
url='https://www.djangoproject.com/')
Tag.objects.create(tagged_item=self.bookmark, tag='django')
Tag.objects.create(tagged_item=self.bookmark, tag='python')
self.note = Note.objects.create(text='Remember the milk')
Tag.objects.create(tagged_item=self.note, tag='reminder')
Detachable.objects.create(content_object=self.note, name='attached')
Detachable.objects.create(name='detached')
def test_relations_as_hyperlinks(self):
class TagSerializer(serializers.ModelSerializer):
tagged_item = GenericRelatedField(
{
Bookmark: serializers.HyperlinkedRelatedField(
view_name='bookmark-detail',
queryset=Bookmark.objects.all()),
Note: serializers.HyperlinkedRelatedField(
view_name='note-detail',
queryset=Note.objects.all()),
},
read_only=True,
)
class Meta:
model = Tag
exclude = ('id', 'content_type', 'object_id', )
serializer = TagSerializer(Tag.objects.all(), many=True, context={'request': request})
expected = [
{
'tagged_item': 'http://testserver/bookmark/1/',
'tag': 'django',
},
{
'tagged_item': 'http://testserver/bookmark/1/',
'tag': 'python',
},
{
'tagged_item': 'http://testserver/note/1/',
'tag': 'reminder'
}
]
self.assertEqual(serializer.data, expected)
def test_relations_as_nested(self):
class TagSerializer(serializers.ModelSerializer):
tagged_item = GenericRelatedField({
Bookmark: BookmarkSerializer(),
Note: NoteSerializer(),
}, read_only=True)
class Meta:
model = Tag
exclude = ('id', 'content_type', 'object_id', )
serializer = TagSerializer(Tag.objects.all(), many=True)
expected = [
{
'tagged_item': {
'url': 'https://www.djangoproject.com/'
},
'tag': 'django'
},
{
'tagged_item': {
'url': 'https://www.djangoproject.com/'
},
'tag': 'python'
},
{
'tagged_item': {
'text': 'Remember the milk',
},
'tag': 'reminder'
}
]
self.assertEqual(serializer.data, expected)
def test_mixed_serializers(self):
class TagSerializer(serializers.ModelSerializer):
tagged_item = GenericRelatedField(
{
Bookmark: BookmarkSerializer(),
Note: serializers.HyperlinkedRelatedField(
view_name='note-detail',
queryset=Note.objects.all()),
},
read_only=True,
)
class Meta:
model = Tag
exclude = ('id', 'content_type', 'object_id', )
serializer = TagSerializer(Tag.objects.all(), many=True, context={'request': request})
expected = [
{
'tagged_item': {
'url': 'https://www.djangoproject.com/'
},
'tag': 'django'
},
{
'tagged_item': {
'url': 'https://www.djangoproject.com/'
},
'tag': 'python'
},
{
'tagged_item': 'http://testserver/note/1/',
'tag': 'reminder'
}
]
self.assertEqual(serializer.data, expected)
def test_invalid_model(self):
# Leaving out the Note model should result in a ValidationError
class TagSerializer(serializers.ModelSerializer):
tagged_item = GenericRelatedField({
Bookmark: BookmarkSerializer(),
}, read_only=True)
class Meta:
model = Tag
exclude = ('id', 'content_type', 'object_id', )
serializer = TagSerializer(Tag.objects.all(), many=True)
with self.assertRaises(serializers.ValidationError):
serializer.data
def test_relation_as_null(self):
class DetachableSerializer(serializers.ModelSerializer):
content_object = GenericRelatedField(
{
Bookmark: serializers.HyperlinkedRelatedField(
view_name='bookmark-detail',
queryset=Bookmark.objects.all()),
Note: serializers.HyperlinkedRelatedField(
view_name='note-detail',
queryset=Note.objects.all()),
},
read_only=True,
)
class Meta:
model = Detachable
exclude = ('id', 'content_type', 'object_id', )
serializer = DetachableSerializer(Detachable.objects.all(), many=True, context={'request': request})
expected = [
{
'content_object': 'http://testserver/note/1/',
'name': 'attached',
},
{
'content_object': None,
'name': 'detached',
}
]
self.assertEqual(serializer.data, expected)
def test_deprecated_method_overridden(self):
with warnings.catch_warnings(record=True) as w:
class MyRelatedField(GenericRelatedField):
def determine_deserializer_for_data(self, value):
return super(MyRelatedField, self).determine_deserializer_for_data(value)
self.assertEqual(len(w), 1)
self.assertIs(w[0].category, DeprecationWarning)
def test_deprecated_method_called(self):
f = GenericRelatedField({
Bookmark: serializers.HyperlinkedRelatedField(
view_name='bookmark-detail',
queryset=Bookmark.objects.all()),
Note: serializers.HyperlinkedRelatedField(
view_name='note-detail',
queryset=Note.objects.all()),
})
with warnings.catch_warnings(record=True) as w:
f.determine_deserializer_for_data(self.bookmark)
self.assertEqual(len(w), 1)
self.assertIs(w[0].category, DeprecationWarning)
def test_subclass_uses_registered_parent(self):
tagged_item = GenericRelatedField({
Note: NoteSerializer(),
}, read_only=True)
# NoteProxy instance should use the NoteSerializer,
# since no more specific serializer is registered
proxied = NoteProxy.objects.get(pk=self.note.pk)
serializer = tagged_item.get_serializer_for_instance(proxied)
self.assertIsInstance(serializer, NoteSerializer)
def test_subclass_uses_registered_subclass(self):
tagged_item = GenericRelatedField({
Note: NoteSerializer(),
NoteProxy: NoteProxySerializer(),
}, read_only=True)
# NoteProxy instance should use the NoteProxySerializer in
# preference to the NoteSerializer
proxied = NoteProxy.objects.get(pk=self.note.pk)
serializer = tagged_item.get_serializer_for_instance(proxied)
self.assertIsInstance(serializer, NoteProxySerializer)
# But Note instance should use the NoteSerializer
serializer = tagged_item.get_serializer_for_instance(self.note)
self.assertIsInstance(serializer, NoteSerializer)
@override_settings(ROOT_URLCONF='generic_relations.tests.test_relations')
class TestGenericRelatedFieldDeserialization(TestCase):
def setUp(self):
self.bookmark = Bookmark.objects.create(
url='https://www.djangoproject.com/')
Tag.objects.create(tagged_item=self.bookmark, tag='django')
Tag.objects.create(tagged_item=self.bookmark, tag='python')
self.note = Note.objects.create(text='Remember the milk')
def test_hyperlink_serialization(self):
class TagSerializer(serializers.ModelSerializer):
tagged_item = GenericRelatedField(
{
Bookmark: serializers.HyperlinkedRelatedField(
view_name='bookmark-detail',
queryset=Bookmark.objects.all()),
Note: serializers.HyperlinkedRelatedField(
view_name='note-detail',
queryset=Note.objects.all()),
},
read_only=False,
)
class Meta:
model = Tag
exclude = ('id', 'content_type', 'object_id', )
serializer = TagSerializer(data={
'tag': 'reminder',
'tagged_item': reverse('note-detail', kwargs={'pk': self.note.pk})
}, context={'request': request})
serializer.is_valid(raise_exception=True)
expected = {
'tagged_item': 'http://testserver/note/1/',
'tag': 'reminder'
}
self.assertEqual(serializer.data, expected)
def test_configuration_error(self):
class TagSerializer(serializers.ModelSerializer):
tagged_item = GenericRelatedField(
{
Bookmark: BookmarkSerializer(),
Note: serializers.HyperlinkedRelatedField(
view_name='note-detail',
queryset=Note.objects.all()),
},
read_only=False,
)
class Meta:
model = Tag
exclude = ('id', 'content_type', 'object_id', )
serializer = TagSerializer(data={
'tag': 'reminder',
'tagged_item': 'just a string'
})
with self.assertRaises(ImproperlyConfigured):
tagged_item = serializer.fields['tagged_item']
tagged_item.get_deserializer_for_data('just a string')
def test_not_registered_view_name(self):
class TagSerializer(serializers.ModelSerializer):
tagged_item = GenericRelatedField(
{
Bookmark: serializers.HyperlinkedRelatedField(
view_name='bookmark-detail',
queryset=Bookmark.objects.all()),
},
read_only=False
)
class Meta:
model = Tag
exclude = ('id', 'content_type', 'object_id', )
serializer = TagSerializer(data={
'tag': 'reminder',
'tagged_item': reverse('note-detail', kwargs={'pk': self.note.pk})
})
self.assertFalse(serializer.is_valid())
def test_invalid_url(self):
class TagSerializer(serializers.ModelSerializer):
tagged_item = GenericRelatedField(
{
Bookmark: serializers.HyperlinkedRelatedField(
view_name='bookmark-detail',
queryset=Bookmark.objects.all()),
},
read_only=False,
)
class Meta:
model = Tag
exclude = ('id', 'content_type', 'object_id', )
serializer = TagSerializer(data={
'tag': 'reminder',
'tagged_item': 'foo-bar'
})
message = 'Could not determine a valid serializer for value %r.'
expected = {'tagged_item': {api_settings.NON_FIELD_ERRORS_KEY: message % 'foo-bar'}}
self.assertFalse(serializer.is_valid())
self.assertEqual(expected, serializer.errors)
def test_serializer_save(self):
class TagSerializer(serializers.ModelSerializer):
tagged_item = GenericRelatedField(
{
Bookmark: serializers.HyperlinkedRelatedField(
view_name='bookmark-detail',
queryset=Bookmark.objects.all()),
Note: serializers.HyperlinkedRelatedField(
view_name='note-detail',
queryset=Note.objects.all()),
},
read_only=False,
)
class Meta:
model = Tag
exclude = ('id', 'content_type', 'object_id', )
serializer = TagSerializer(data={
'tag': 'reminder',
'tagged_item': reverse('note-detail', kwargs={'pk': self.note.pk})
})
serializer.is_valid(raise_exception=True)
serializer.save()
tag = Tag.objects.get(pk=3)
self.assertEqual(tag.tagged_item, self.note)
def test_nullable_relation_serializer_save(self):
class DetachableSerializer(serializers.ModelSerializer):
content_object = GenericRelatedField(
{
Bookmark: serializers.HyperlinkedRelatedField(
view_name='bookmark-detail',
queryset=Bookmark.objects.all()),
Note: serializers.HyperlinkedRelatedField(
view_name='note-detail',
queryset=Note.objects.all()),
},
read_only=False,
required=False
)
class Meta:
model = Detachable
exclude = ('id', 'content_type', 'object_id', )
serializer = DetachableSerializer(data={'name': 'foo'})
serializer.is_valid(raise_exception=True)
serializer.save()
freeagent = Detachable.objects.get(pk=1)
self.assertEqual(freeagent.name, 'foo')
self.assertEqual(freeagent.content_object, None)
def test_deprecated_method_overridden(self):
with warnings.catch_warnings(record=True) as w:
class MyRelatedField(GenericRelatedField):
def determine_serializer_for_data(self, value):
return super(MyRelatedField, self).determine_serializer_for_data(value)
self.assertEqual(len(w), 1)
self.assertIs(w[0].category, DeprecationWarning)
def test_deprecated_method_called(self):
f = GenericRelatedField({
Bookmark: serializers.HyperlinkedRelatedField(
view_name='bookmark-detail',
queryset=Bookmark.objects.all()),
})
with warnings.catch_warnings(record=True) as w:
f.determine_serializer_for_data('http://testserver/bookmark/1/')
self.assertEqual(len(w), 1)
self.assertIs(w[0].category, DeprecationWarning)
class TestGenericRelatedField(TestCase):
def test_multiple_declaration(self):
with self.assertRaises(RuntimeError):
class TagSerializer(serializers.ModelSerializer):
fields = {
Bookmark: BookmarkSerializer(),
Note: NoteSerializer(),
}
first = GenericRelatedField(fields)
second = GenericRelatedField(fields)
class Meta:
model = Tag
================================================
FILE: generic_relations/tests/test_serializers.py
================================================
from django.test import TestCase
from rest_framework import serializers
from generic_relations.serializers import GenericModelSerializer
from generic_relations.tests.models import Bookmark, Note
from .test_relations import BookmarkSerializer, NoteSerializer
class TestGenericModelSerializer(TestCase):
def setUp(self):
self.bookmark = Bookmark.objects.create(
url='https://www.djangoproject.com/')
self.note = Note.objects.create(text='Remember the milk')
self.note2 = Note.objects.create(text='Reticulate the splines')
self.serializer = GenericModelSerializer(
{
Bookmark: BookmarkSerializer(),
Note: NoteSerializer(),
}
)
self.list_serializer = serializers.ListSerializer(child=self.serializer)
def test_serialize(self):
self.assertEqual(
self.serializer.to_representation(self.bookmark),
{'url': 'https://www.djangoproject.com/'},
)
self.assertEqual(
self.serializer.to_representation(self.note),
{'text': 'Remember the milk'},
)
def test_deserialize(self):
self.assertEqual(
self.serializer.to_internal_value({'url': 'https://www.djangoproject.com/'}),
{'url': 'https://www.djangoproject.com/'},
)
self.assertEqual(
self.serializer.to_internal_value({'text': 'Remember the milk'}),
{'text': 'Remember the milk'},
)
def test_serialize_list(self):
actual = self.list_serializer.to_representation([
self.bookmark, self.note, self.note2, self.bookmark,
])
expected = [
{'url': 'https://www.djangoproject.com/'},
{'text': 'Remember the milk'},
{'text': 'Reticulate the splines'},
{'url': 'https://www.djangoproject.com/'},
]
self.assertEqual(actual, expected)
def test_deserialize_list(self):
validated_data = self.list_serializer.to_internal_value([
{'url': 'https://www.djangoproject.com/'},
{'text': 'Remember the milk'},
{'text': 'Reticulate the splines'},
{'url': 'https://www.djangoproject.com/'},
])
self.assertEqual(validated_data, [
{'url': 'https://www.djangoproject.com/'},
{'text': 'Remember the milk'},
{'text': 'Reticulate the splines'},
{'url': 'https://www.djangoproject.com/'},
])
def test_is_valid_raise_exception(self):
serializer = GenericModelSerializer(
serializers={Bookmark: BookmarkSerializer()},
data={'url': 'not-a-url'},
)
with self.assertRaises(serializers.ValidationError):
serializer.is_valid(raise_exception=True)
================================================
FILE: manage.py
================================================
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testsettings")
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)
================================================
FILE: setup.cfg
================================================
[bdist_wheel]
universal = 1
================================================
FILE: setup.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from setuptools import setup, find_packages
from os.path import abspath, dirname, join
def read_relative_file(filename):
"""
Returns contents of the given file, whose path is supposed relative
to this module.
"""
with open(join(dirname(abspath(__file__)), filename), "r") as f:
return f.read()
setup(
name="rest-framework-generic-relations",
version="2.1.0",
url="https://github.com/Ian-Foote/rest-framework-generic-relations",
license="BSD",
description="Generic Relations for Django Rest Framework",
long_description=read_relative_file("README.md"),
long_description_content_type="text/markdown",
author="Ian Foote",
author_email="python@ian.feete.org",
packages=find_packages(),
include_package_data=True,
install_requires=["djangorestframework>=3.11.0"],
python_requires=">=3.6",
classifiers=[
"Environment :: Web Environment",
"Framework :: Django",
"Intended Audience :: Developers",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Framework :: Django :: 2.2",
"Framework :: Django :: 3.0",
"Framework :: Django :: 3.1",
"Framework :: Django :: 3.2",
"Framework :: Django :: 4.0",
"Framework :: Django :: 4.1",
],
)
================================================
FILE: testsettings.py
================================================
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
},
}
INSTALLED_APPS = (
'django.contrib.contenttypes',
'generic_relations',
'generic_relations.tests',
)
ROOT_URLCONF = ''
SECRET_KEY = 'abcde12345'
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware'
)
================================================
FILE: tox.ini
================================================
[tox]
envlist =
# note: min/max python versions specified here;
# testing in-between versions here seems a waste of resources.
{py37,py39}-{dj22,dj31,dj32}-{drf311,drf312}
py311-{dj32,dj41}-{drf313,drf314}
[testenv]
changedir = {toxinidir}
commands = pytest --ds=testsettings {posargs}
deps =
pytest-django
dj22: Django~=2.2.17
dj31: Django~=3.1.0
dj32: Django~=3.2.0
dj41: Django~=4.1.0
drf311: djangorestframework~=3.11.0
drf312: djangorestframework~=3.12.0
drf313: djangorestframework~=3.13.0
drf314: djangorestframework~=3.14.0
gitextract_9csql789/ ├── .github/ │ └── workflows/ │ └── tests.yml ├── .gitignore ├── Changelog.md ├── Makefile ├── README.md ├── generic_relations/ │ ├── __init__.py │ ├── relations.py │ ├── serializers.py │ └── tests/ │ ├── __init__.py │ ├── migrations/ │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── test_relations.py │ └── test_serializers.py ├── manage.py ├── setup.cfg ├── setup.py ├── testsettings.py └── tox.ini
SYMBOL INDEX (58 symbols across 7 files)
FILE: generic_relations/relations.py
class RenamedMethods (line 11) | class RenamedMethods(RenameMethodsBase):
class GenericRelatedField (line 18) | class GenericRelatedField(GenericSerializerMixin, serializers.Field, met...
FILE: generic_relations/serializers.py
class GenericSerializerMixin (line 12) | class GenericSerializerMixin(object):
method __init__ (line 22) | def __init__(self, serializers, *args, **kwargs):
method to_internal_value (line 35) | def to_internal_value(self, data):
method to_representation (line 42) | def to_representation(self, instance):
method get_serializer_for_instance (line 46) | def get_serializer_for_instance(self, instance):
method get_deserializer_for_data (line 56) | def get_deserializer_for_data(self, value):
class GenericModelSerializer (line 80) | class GenericModelSerializer(GenericSerializerMixin, serializers.Seriali...
FILE: generic_relations/tests/migrations/0001_initial.py
class Migration (line 9) | class Migration(migrations.Migration):
FILE: generic_relations/tests/models.py
class Tag (line 7) | class Tag(models.Model):
method __unicode__ (line 16) | def __unicode__(self):
class Detachable (line 20) | class Detachable(models.Model):
class Bookmark (line 31) | class Bookmark(models.Model):
method __unicode__ (line 38) | def __unicode__(self):
class Note (line 42) | class Note(models.Model):
method __unicode__ (line 49) | def __unicode__(self):
class NoteProxy (line 53) | class NoteProxy(Note):
class Meta (line 54) | class Meta:
FILE: generic_relations/tests/test_relations.py
function dummy_view (line 30) | def dummy_view(request, pk):
class BookmarkSerializer (line 46) | class BookmarkSerializer(serializers.ModelSerializer):
class Meta (line 47) | class Meta:
class NoteSerializer (line 52) | class NoteSerializer(serializers.ModelSerializer):
class Meta (line 53) | class Meta:
class NoteProxySerializer (line 58) | class NoteProxySerializer(serializers.ModelSerializer):
class Meta (line 61) | class Meta:
method get_text (line 65) | def get_text(self, instance):
class TestGenericRelatedFieldSerialization (line 70) | class TestGenericRelatedFieldSerialization(TestCase):
method setUp (line 71) | def setUp(self):
method test_relations_as_hyperlinks (line 82) | def test_relations_as_hyperlinks(self):
method test_relations_as_nested (line 118) | def test_relations_as_nested(self):
method test_mixed_serializers (line 153) | def test_mixed_serializers(self):
method test_invalid_model (line 190) | def test_invalid_model(self):
method test_relation_as_null (line 205) | def test_relation_as_null(self):
method test_deprecated_method_overridden (line 236) | def test_deprecated_method_overridden(self):
method test_deprecated_method_called (line 245) | def test_deprecated_method_called(self):
method test_subclass_uses_registered_parent (line 260) | def test_subclass_uses_registered_parent(self):
method test_subclass_uses_registered_subclass (line 271) | def test_subclass_uses_registered_subclass(self):
class TestGenericRelatedFieldDeserialization (line 289) | class TestGenericRelatedFieldDeserialization(TestCase):
method setUp (line 290) | def setUp(self):
method test_hyperlink_serialization (line 297) | def test_hyperlink_serialization(self):
method test_configuration_error (line 326) | def test_configuration_error(self):
method test_not_registered_view_name (line 351) | def test_not_registered_view_name(self):
method test_invalid_url (line 372) | def test_invalid_url(self):
method test_serializer_save (line 398) | def test_serializer_save(self):
method test_nullable_relation_serializer_save (line 425) | def test_nullable_relation_serializer_save(self):
method test_deprecated_method_overridden (line 451) | def test_deprecated_method_overridden(self):
method test_deprecated_method_called (line 460) | def test_deprecated_method_called(self):
class TestGenericRelatedField (line 473) | class TestGenericRelatedField(TestCase):
method test_multiple_declaration (line 474) | def test_multiple_declaration(self):
FILE: generic_relations/tests/test_serializers.py
class TestGenericModelSerializer (line 13) | class TestGenericModelSerializer(TestCase):
method setUp (line 14) | def setUp(self):
method test_serialize (line 28) | def test_serialize(self):
method test_deserialize (line 38) | def test_deserialize(self):
method test_serialize_list (line 48) | def test_serialize_list(self):
method test_deserialize_list (line 60) | def test_deserialize_list(self):
method test_is_valid_raise_exception (line 75) | def test_is_valid_raise_exception(self):
FILE: setup.py
function read_relative_file (line 8) | def read_relative_file(filename):
Condensed preview — 19 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (44K chars).
[
{
"path": ".github/workflows/tests.yml",
"chars": 1757,
"preview": "name: tests\non: [push, pull_request]\njobs:\n build:\n runs-on: ubuntu-latest\n strategy:\n matrix:\n pytho"
},
{
"path": ".gitignore",
"chars": 62,
"preview": "*.pyc\n__pycache__\nMANIFEST\ndist/\nbuild/\n*.egg-info/\n.eggs\n.tox"
},
{
"path": "Changelog.md",
"chars": 1113,
"preview": "# Rest Framework Generic Relations Changelog\n\n## v2.1.0\n\nGeneral dependency update\n\n* Minimum Python version is now 3.6\n"
},
{
"path": "Makefile",
"chars": 256,
"preview": "SHELL := /bin/bash\n\nhelp:\n\t@echo \"Usage:\"\n\t@echo \" make help -- displays this help\"\n\t@echo \" make test -- runs tes"
},
{
"path": "README.md",
"chars": 7191,
"preview": "# Rest Framework Generic Relations [\ndistribution = pkg_resources.get_distribution('rest-framework-generic-relati"
},
{
"path": "generic_relations/relations.py",
"chars": 725,
"preview": "from django.utils.deprecation import RenameMethodsBase\n\nfrom rest_framework import serializers\n\nfrom .serializers import"
},
{
"path": "generic_relations/serializers.py",
"chars": 3395,
"preview": "from django.core.exceptions import ImproperlyConfigured\nfrom django.utils.translation import gettext_lazy as _\nfrom djan"
},
{
"path": "generic_relations/tests/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "generic_relations/tests/migrations/0001_initial.py",
"chars": 2072,
"preview": "# -*- coding: utf-8 -*-\n# Generated by Django 1.11.22 on 2019-08-01 04:03\n\n\nfrom django.db import migrations, models\nimp"
},
{
"path": "generic_relations/tests/migrations/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "generic_relations/tests/models.py",
"chars": 1472,
"preview": "from django.contrib.contenttypes.models import ContentType\nfrom django.contrib.contenttypes.fields import GenericForeign"
},
{
"path": "generic_relations/tests/test_relations.py",
"chars": 17265,
"preview": "\n\nimport warnings\n\ntry:\n from django.urls import re_path as url\nexcept ImportError:\n from django.conf.urls import "
},
{
"path": "generic_relations/tests/test_serializers.py",
"chars": 2852,
"preview": "\n\nfrom django.test import TestCase\n\nfrom rest_framework import serializers\n\nfrom generic_relations.serializers import Ge"
},
{
"path": "manage.py",
"chars": 246,
"preview": "#!/usr/bin/env python\nimport os\nimport sys\n\nif __name__ == \"__main__\":\n os.environ.setdefault(\"DJANGO_SETTINGS_MODULE"
},
{
"path": "setup.cfg",
"chars": 28,
"preview": "[bdist_wheel]\nuniversal = 1\n"
},
{
"path": "setup.py",
"chars": 1809,
"preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\nfrom setuptools import setup, find_packages\nfrom os.path import abspath, "
},
{
"path": "testsettings.py",
"chars": 399,
"preview": "DATABASES = {\n 'default': {\n 'ENGINE': 'django.db.backends.sqlite3',\n 'NAME': ':memory:',\n },\n}\n\nINS"
},
{
"path": "tox.ini",
"chars": 584,
"preview": "[tox]\nenvlist =\n # note: min/max python versions specified here;\n # testing in-between versions here seems a waste"
}
]
About this extraction
This page contains the full source code of the Ian-Foote/rest-framework-generic-relations GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 19 files (40.4 KB), approximately 9.0k tokens, and a symbol index with 58 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.