[
  {
    "path": ".github/workflows/tests.yml",
    "content": "name: tests\non: [push, pull_request]\njobs:\n  build:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        python-version:\n          - \"3.7\"\n          - \"3.11\"\n        django-version:\n          - \"2.2\"\n          - \"3.1\"\n          - \"3.2\"\n          - \"4.1\"\n        drf-version:\n          - \"3.11\"\n          - \"3.12\"\n          - \"3.13\"\n          - \"3.14\"\n        exclude:\n          - django-version: \"2.2\"\n            drf-version: \"3.13\"\n          - django-version: \"3.1\"\n            drf-version: \"3.13\"\n          - django-version: \"2.2\"\n            drf-version: \"3.14\"\n          - django-version: \"3.1\"\n            drf-version: \"3.14\"\n          - django-version: \"4.1\"\n            drf-version: \"3.11\"\n          - django-version: \"4.1\"\n            drf-version: \"3.12\"\n          - django-version: \"4.1\"\n            python-version: \"3.7\"\n\n    name: \"py${{ matrix.python-version}} dj${{ matrix.django-version }} drf${{ matrix.drf-version }}\"\n\n    steps:\n      - uses: actions/checkout@v2\n      - name: Set up Python ${{ matrix.python-version }}\n        uses: actions/setup-python@v2\n        with:\n          python-version: ${{ matrix.python-version }}\n      - name: Install dependencies (django ${{ matrix.django-version }})\n        run: |\n          python -m pip install --upgrade pip\n          pip install flake8 pytest-django django==${{ matrix.django-version }}.* djangorestframework==${{ matrix.drf-version }}.* \n          pip install -e .\n      - name: Lint with flake8\n        run: |\n          # stop the build if there are Python syntax errors or undefined names\n          flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics\n      - name: Test with pytest\n        run: |\n          pytest --ds=testsettings generic_relations/tests/\n"
  },
  {
    "path": ".gitignore",
    "content": "*.pyc\n__pycache__\nMANIFEST\ndist/\nbuild/\n*.egg-info/\n.eggs\n.tox"
  },
  {
    "path": "Changelog.md",
    "content": "# Rest Framework Generic Relations Changelog\n\n## v2.1.0\n\nGeneral dependency update\n\n* Minimum Python version is now 3.6\n* Minimum DRF version is now 3.11\n* Supported Django versions are now [2.2, 3.1, 3.2]\n\n## v2.0.0\n\n* Add Python 3.8, Django 3.0 and DRF 3.11 support\n* Drop Python 2\n* Drop DRF 3.7\n\nMinimum supported dependencies are now:\n* Python 3.4\n* Django 1.11\n* DRF 3.8\n\n## v1.2.1\n* Add error handling for reusing a `Serializer` instance in a `GenericRelatedField`.\n\n## v1.2.0\n\n* Add Django 2.0 support (need Python min 3.4, Django min. 1.11).\n\n## v1.1.0\n\n* Add `GenericModelSerializer` as a counterpart to `GenericRelatedField`.\n* Dynamically determine the best serializer to use to serialize a model instance.\n* Rename `determine_serializer_for_data` to `get_deserializer_for_data`\n* Rename `determine_deserializer_for_data` to `get_serializer_for_instance`\n\n## v1.0.0\n\n* Add support for Django 1.8 and 1.9\n* Add support for Django Rest Framework 3\n* Drop support for earlier versions of Django and DRF\n\n## v0.1.0\n\n* Initial release - Forked from https://github.com/encode/django-rest-framework/pull/755\n"
  },
  {
    "path": "Makefile",
    "content": "SHELL := /bin/bash\n\nhelp:\n\t@echo \"Usage:\"\n\t@echo \" make help    -- displays this help\"\n\t@echo \" make test    -- runs tests\"\n\t@echo \" make release -- pushes to pypi\"\n\ntest:\n\ttox\n\nrelease:\n\trm -rf dist\n\tpython setup.py sdist bdist_wheel\n\ttwine upload dist/*\n"
  },
  {
    "path": "README.md",
    "content": "# Rest Framework Generic Relations [![Build Status](https://github.com/Ian-Foote/rest-framework-generic-relations/actions/workflows/tests.yml/badge.svg)](https://github.com/Ian-Foote/rest-framework-generic-relations/actions/workflows/tests.yml)\n\n\nThis library implements [Django REST Framework](http://www.django-rest-framework.org/) serializers to handle generic foreign keys.\n\n# Requirements\n\nAny currently-supported combination of Django REST Framework, Python, and Django.\n\n# Installation\n\nInstall using `pip`...\n```sh\npip install  rest-framework-generic-relations\n```\nAdd `'generic_relations'` to your `INSTALLED_APPS` setting.\n```python\nINSTALLED_APPS = (\n    ...\n    'generic_relations',\n)\n```\n\n\n# API Reference\n\n## GenericRelatedField\n\nThis field serializes generic foreign keys. For a primer on generic foreign keys, first see: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/\n\n\nLet's assume a `TaggedItem` model which has a generic relationship with other arbitrary models:\n\n```python\nclass TaggedItem(models.Model):\n    tag_name = models.SlugField()\n    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)\n    object_id = models.PositiveIntegerField()\n    tagged_object = GenericForeignKey('content_type', 'object_id')\n```\n\nAnd the following two models, which may have associated tags:\n\n```python\nclass Bookmark(models.Model):\n    \"\"\"\n    A bookmark consists of a URL, and 0 or more descriptive tags.\n    \"\"\"\n    url = models.URLField()\n    tags = GenericRelation(TaggedItem)\n\nclass Note(models.Model):\n    \"\"\"\n    A note consists of some text, and 0 or more descriptive tags.\n    \"\"\"\n    text = models.CharField(max_length=1000)\n    tags = GenericRelation(TaggedItem)\n```\n\nNow we define serializers for each model that may get associated with tags.\n\n```python\nclass BookmarkSerializer(serializers.ModelSerializer):\n    class Meta:\n        model = Bookmark\n        fields = ('url',)\n\nclass NoteSerializer(serializers.ModelSerializer):\n    class Meta:\n        model = Note\n        fields = ('text',)\n```\n\nThe model serializer for the `TaggedItem` model could look like this:\n\n```python\nfrom generic_relations.relations import GenericRelatedField\n\nclass TagSerializer(serializers.ModelSerializer):\n    \"\"\"\n    A `TaggedItem` serializer with a `GenericRelatedField` mapping all possible\n    models to their respective serializers.\n    \"\"\"\n    tagged_object = GenericRelatedField({\n        Bookmark: BookmarkSerializer(),\n        Note: NoteSerializer()\n    })\n\n    class Meta:\n        model = TaggedItem\n        fields = ('tag_name', 'tagged_object')\n```\n\nThe 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:\n\n```json\n{\n    \"tagged_object\": {\n        \"url\": \"https://www.djangoproject.com/\"\n    },\n    \"tag_name\": \"django\"\n}\n```\n\nIf you want to have your generic foreign key represented as hyperlink, simply use `HyperlinkedRelatedField` objects:\n\n```python\nclass TagSerializer(serializers.ModelSerializer):\n    \"\"\"\n    A `Tag` serializer with a `GenericRelatedField` mapping all possible\n    models to properly set up `HyperlinkedRelatedField`s.\n    \"\"\"\n    tagged_object = GenericRelatedField({\n        Bookmark: serializers.HyperlinkedRelatedField(\n            queryset = Bookmark.objects.all(),\n            view_name='bookmark-detail',\n        ),\n        Note: serializers.HyperlinkedRelatedField(\n            queryset = Note.objects.all(),\n            view_name='note-detail',\n        ),\n    })\n\n    class Meta:\n        model = TaggedItem\n        fields = ('tag_name', 'tagged_object')\n```\n\nThe JSON representation of the same `TaggedItem` example object could now look something like this:\n\n```json\n{\n    \"tagged_object\": \"/bookmark/1/\",\n    \"tag_name\": \"django\"\n}\n```\n\n## Writing to generic foreign keys\n\nThe 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.\nNote, that (at the moment) only `HyperlinkedRelatedField` is able to serialize model objects out of the box.\n\n\nThe 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.\n\n```python\ntag_serializer = TagSerializer(data={\n    'tag_name': 'python',\n    'tagged_object': '/bookmark/1/'\n})\n\ntag_serializer.is_valid()\ntag_serializer.save()\n```\n\nIf 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.\n\n## GenericModelSerializer\n\nSometimes 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:\n\n```python\nfrom django.core.validators import MaxValueValidator\n\nclass Book(models.Model):\n    title = models.CharField(max_length=255)\n    author = models.CharField(max_length=255)\n\nclass Bluray(models.Model):\n    title = models.CharField(max_length=255)\n    rating = models.PositiveSmallIntegerField(\n        validators=[MaxValueValidator(5)],\n    )\n```\n\nThen we could have a serializer for each type of object:\n\n```python\nclass BookSerializer(serializers.ModelSerializer):\n    class Meta:\n        model = Book\n        fields = ('title', 'author')\n\nclass BluraySerializer(serializers.ModelSerializer):\n    class Meta:\n        model = Bluray\n        fields = ('title', 'rating')\n```\n\nNow we can create a generic list serializer, which delegates to the above serializers based on the type of model it's serializing:\n\n```python\nbookshelf_item_serializer = GenericModelSerializer(\n    {\n        Book: BookSerializer(),\n        Bluray: BluraySerializer(),\n    },\n    many=True,\n)\n```\n\nThen we can serialize a mixed list of items:\n\n```python\n>>> bookshelf_item_serializer.to_representation([\n    Book.objects.get(title='War and Peace'),\n    Bluray.objects.get(title='Die Hard'),\n    Bluray.objects.get(title='Shawshank Redemption'),\n    Book.objects.get(title='To Kill a Mockingbird'),\n])\n\n[\n    {'title': 'War and Peace', 'author': 'Leo Tolstoy'},\n    {'title': 'Die Hard', 'rating': 5},\n    {'title': 'Shawshank Redemption', 'rating': 5},\n    {'title': 'To Kill a Mockingbird', 'author': 'Harper Lee'}\n]\n```\n\n\n## A few things you should note:\n\n* Although `GenericForeignKey` fields can be set to any model object, the `GenericRelatedField` only handles models explicitly defined in its configuration dictionary.\n* 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.\n* The order in which you register serializers matters as far as write operations are concerned.\n* Unless you provide a custom `get_deserializer_for_data()` method, only `HyperlinkedRelatedField` provides write access to generic model relations.\n"
  },
  {
    "path": "generic_relations/__init__.py",
    "content": "pkg_resources = __import__('pkg_resources')\ndistribution = pkg_resources.get_distribution('rest-framework-generic-relations')\n\n__version__ = distribution.version\n"
  },
  {
    "path": "generic_relations/relations.py",
    "content": "from django.utils.deprecation import RenameMethodsBase\n\nfrom rest_framework import serializers\n\nfrom .serializers import GenericSerializerMixin\n\n\n__all__ = ('GenericRelatedField',)\n\n\nclass RenamedMethods(RenameMethodsBase):\n    renamed_methods = (\n        ('determine_deserializer_for_data', 'get_serializer_for_instance', DeprecationWarning),\n        ('determine_serializer_for_data', 'get_deserializer_for_data', DeprecationWarning),\n    )\n\n\nclass GenericRelatedField(GenericSerializerMixin, serializers.Field, metaclass=RenamedMethods):\n    \"\"\"\n    Represents a generic relation / foreign key.\n    It's actually more of a wrapper, that delegates the logic to registered\n    serializers based on the `Model` class.\n    \"\"\"\n"
  },
  {
    "path": "generic_relations/serializers.py",
    "content": "from django.core.exceptions import ImproperlyConfigured\nfrom django.utils.translation import gettext_lazy as _\nfrom django import forms\n\nfrom rest_framework import serializers\nfrom rest_framework.settings import api_settings\n\n\n__all__ = ('GenericSerializerMixin', 'GenericModelSerializer',)\n\n\nclass GenericSerializerMixin(object):\n    default_error_messages = {\n        'no_model_match': _('Invalid model - model not available.'),\n        'no_url_match': _('Invalid hyperlink - No URL match'),\n        'incorrect_url_match': _(\n            'Invalid hyperlink - view name not available'),\n    }\n\n    form_field_class = forms.URLField\n\n    def __init__(self, serializers, *args, **kwargs):\n        \"\"\"\n        Needs an extra parameter `serializers` which has to be a dict\n        key: value being `Model`: serializer.\n        \"\"\"\n        super(GenericSerializerMixin, self).__init__(*args, **kwargs)\n        self.serializers = serializers\n        for serializer in self.serializers.values():\n            if serializer.source is not None:\n                msg = '{}() cannot be re-used. Create a new instance.'\n                raise RuntimeError(msg.format(type(serializer).__name__))\n            serializer.bind('', self)\n\n    def to_internal_value(self, data):\n        try:\n            serializer = self.get_deserializer_for_data(data)\n        except ImproperlyConfigured as e:\n            raise serializers.ValidationError({api_settings.NON_FIELD_ERRORS_KEY: e})\n        return serializer.to_internal_value(data)\n\n    def to_representation(self, instance):\n        serializer = self.get_serializer_for_instance(instance)\n        return serializer.to_representation(instance)\n\n    def get_serializer_for_instance(self, instance):\n        # Use registered superclasses, rather than only the exact model.\n        # (But prefer things earlier in the MRO, so if the exact model is registered,\n        # use that in preference to any superclasses)\n        for klass in instance.__class__.mro():\n            if klass in self.serializers:\n                return self.serializers[klass]\n\n        raise serializers.ValidationError(self.error_messages['no_model_match'])\n\n    def get_deserializer_for_data(self, value):\n        # While one could easily execute the \"try\" block within\n        # to_internal_value and reduce operations, I consider the concept of\n        # serializing is already very naive and vague, that's why I'd\n        # go for stringency with the deserialization process here.\n        serializers = []\n        for serializer in self.serializers.values():\n            try:\n                serializer.to_internal_value(value)\n                # Collects all serializers that can handle the input data.\n                serializers.append(serializer)\n            except Exception:\n                pass\n        # If no serializer found, raise error.\n        l = len(serializers)\n        if l < 1:\n            raise ImproperlyConfigured(\n                'Could not determine a valid serializer for value %r.' % value)\n        elif l > 1:\n            raise ImproperlyConfigured(\n                'There were multiple serializers found for value %r.' % value)\n        return serializers[0]\n\n\nclass GenericModelSerializer(GenericSerializerMixin, serializers.Serializer):\n    \"\"\"\n    Delegates serialization and deserialization to registered serializers\n    based on the type of the model.\n    \"\"\"\n"
  },
  {
    "path": "generic_relations/tests/__init__.py",
    "content": ""
  },
  {
    "path": "generic_relations/tests/migrations/0001_initial.py",
    "content": "# -*- coding: utf-8 -*-\n# Generated by Django 1.11.22 on 2019-08-01 04:03\n\n\nfrom django.db import migrations, models\nimport django.db.models.deletion\n\n\nclass Migration(migrations.Migration):\n\n    initial = True\n\n    dependencies = [\n        ('contenttypes', '0002_remove_content_type_name'),\n    ]\n\n    operations = [\n        migrations.CreateModel(\n            name='Bookmark',\n            fields=[\n                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),\n                ('url', models.URLField()),\n            ],\n        ),\n        migrations.CreateModel(\n            name='Detachable',\n            fields=[\n                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),\n                ('name', models.CharField(max_length=50)),\n                ('object_id', models.PositiveIntegerField(blank=True, null=True)),\n                ('content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),\n            ],\n        ),\n        migrations.CreateModel(\n            name='Note',\n            fields=[\n                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),\n                ('text', models.TextField()),\n            ],\n        ),\n        migrations.CreateModel(\n            name='Tag',\n            fields=[\n                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),\n                ('tag', models.SlugField()),\n                ('object_id', models.PositiveIntegerField()),\n                ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),\n            ],\n        ),\n        migrations.CreateModel(\n            name='NoteProxy',\n            fields=[\n            ],\n            options={\n                'proxy': True,\n                'indexes': [],\n            },\n            bases=('tests.note',),\n        ),\n    ]\n"
  },
  {
    "path": "generic_relations/tests/migrations/__init__.py",
    "content": ""
  },
  {
    "path": "generic_relations/tests/models.py",
    "content": "from django.contrib.contenttypes.models import ContentType\nfrom django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation\n\nfrom django.db import models\n\n\nclass Tag(models.Model):\n    \"\"\"\n    Tags have a descriptive slug, and are attached to an arbitrary object.\n    \"\"\"\n    tag = models.SlugField()\n    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)\n    object_id = models.PositiveIntegerField()\n    tagged_item = GenericForeignKey('content_type', 'object_id')\n\n    def __unicode__(self):\n        return self.tag\n\n\nclass Detachable(models.Model):\n    \"\"\"\n    Model with an optional GenericForeignKey relation\n    \"\"\"\n    name = models.CharField(max_length=50)\n    content_type = models.ForeignKey(\n        ContentType, null=True, blank=True, on_delete=models.CASCADE)\n    object_id = models.PositiveIntegerField(null=True, blank=True)\n    content_object = GenericForeignKey('content_type', 'object_id')\n\n\nclass Bookmark(models.Model):\n    \"\"\"\n    A URL bookmark that may have multiple tags attached.\n    \"\"\"\n    url = models.URLField()\n    tags = GenericRelation(Tag)\n\n    def __unicode__(self):\n        return 'Bookmark: %s' % self.url\n\n\nclass Note(models.Model):\n    \"\"\"\n    A textual note that may have multiple tags attached.\n    \"\"\"\n    text = models.TextField()\n    tags = GenericRelation(Tag)\n\n    def __unicode__(self):\n        return 'Note: %s' % self.text\n\n\nclass NoteProxy(Note):\n    class Meta:\n        proxy = True\n"
  },
  {
    "path": "generic_relations/tests/test_relations.py",
    "content": "\n\nimport warnings\n\ntry:\n    from django.urls import re_path as url\nexcept ImportError:\n    from django.conf.urls import url\n\nfrom django.core.exceptions import ImproperlyConfigured\nfrom django.test import TestCase, RequestFactory\nfrom django.test.utils import override_settings\n\nfrom rest_framework import serializers\nfrom rest_framework.reverse import reverse\nfrom rest_framework.settings import api_settings\n\nfrom generic_relations.relations import GenericRelatedField\nfrom generic_relations.tests.models import Bookmark, Detachable, Note, NoteProxy, Tag\n\n\nwarnings.simplefilter(\"default\", DeprecationWarning)\n\n\nfactory = RequestFactory()\n# Just to ensure we have a request in the serializer context\nrequest = factory.get('/')\n\n\ndef dummy_view(request, pk):\n    pass\n\nurlpatterns = [\n    url(r'^bookmark/(?P<pk>[0-9]+)/$', dummy_view, name='bookmark-detail'),\n    url(r'^detachable/(?P<pk>[0-9]+)/$', dummy_view, name='detachable-detail'),\n    url(r'^note/(?P<pk>[0-9]+)/$', dummy_view, name='note-detail'),\n    url(r'^tag/(?P<pk>[0-9]+)/$', dummy_view, name='tag-detail'),\n    url(\n        r'^contact/(?P<my_own_slug>[-\\w]+)/$',\n        dummy_view,\n        name='contact-detail'\n    ),\n]\n\n\nclass BookmarkSerializer(serializers.ModelSerializer):\n    class Meta:\n        model = Bookmark\n        exclude = ('id', )\n\n\nclass NoteSerializer(serializers.ModelSerializer):\n    class Meta:\n        model = Note\n        exclude = ('id', )\n\n\nclass NoteProxySerializer(serializers.ModelSerializer):\n    text = serializers.SerializerMethodField()\n\n    class Meta:\n        model = NoteProxy\n        exclude = ('id',)\n\n    def get_text(self, instance):\n        return 'proxied: %s' % instance.text\n\n\n@override_settings(ROOT_URLCONF='generic_relations.tests.test_relations')\nclass TestGenericRelatedFieldSerialization(TestCase):\n    def setUp(self):\n        self.bookmark = Bookmark.objects.create(\n            url='https://www.djangoproject.com/')\n        Tag.objects.create(tagged_item=self.bookmark, tag='django')\n        Tag.objects.create(tagged_item=self.bookmark, tag='python')\n        self.note = Note.objects.create(text='Remember the milk')\n        Tag.objects.create(tagged_item=self.note, tag='reminder')\n\n        Detachable.objects.create(content_object=self.note, name='attached')\n        Detachable.objects.create(name='detached')\n\n    def test_relations_as_hyperlinks(self):\n\n        class TagSerializer(serializers.ModelSerializer):\n            tagged_item = GenericRelatedField(\n                {\n                    Bookmark: serializers.HyperlinkedRelatedField(\n                        view_name='bookmark-detail',\n                        queryset=Bookmark.objects.all()),\n                    Note: serializers.HyperlinkedRelatedField(\n                        view_name='note-detail',\n                        queryset=Note.objects.all()),\n                },\n                read_only=True,\n            )\n\n            class Meta:\n                model = Tag\n                exclude = ('id', 'content_type', 'object_id', )\n\n        serializer = TagSerializer(Tag.objects.all(), many=True, context={'request': request})\n        expected = [\n            {\n                'tagged_item': 'http://testserver/bookmark/1/',\n                'tag': 'django',\n            },\n            {\n                'tagged_item': 'http://testserver/bookmark/1/',\n                'tag': 'python',\n            },\n            {\n                'tagged_item': 'http://testserver/note/1/',\n                'tag': 'reminder'\n            }\n        ]\n        self.assertEqual(serializer.data, expected)\n\n    def test_relations_as_nested(self):\n\n        class TagSerializer(serializers.ModelSerializer):\n            tagged_item = GenericRelatedField({\n                Bookmark: BookmarkSerializer(),\n                Note: NoteSerializer(),\n            }, read_only=True)\n\n            class Meta:\n                model = Tag\n                exclude = ('id', 'content_type', 'object_id', )\n\n        serializer = TagSerializer(Tag.objects.all(), many=True)\n        expected = [\n            {\n                'tagged_item': {\n                    'url': 'https://www.djangoproject.com/'\n                },\n                'tag': 'django'\n            },\n            {\n                'tagged_item': {\n                    'url': 'https://www.djangoproject.com/'\n                },\n                'tag': 'python'\n            },\n            {\n                'tagged_item': {\n                    'text': 'Remember the milk',\n                },\n                'tag': 'reminder'\n            }\n        ]\n        self.assertEqual(serializer.data, expected)\n\n    def test_mixed_serializers(self):\n        class TagSerializer(serializers.ModelSerializer):\n            tagged_item = GenericRelatedField(\n                {\n                    Bookmark: BookmarkSerializer(),\n                    Note: serializers.HyperlinkedRelatedField(\n                        view_name='note-detail',\n                        queryset=Note.objects.all()),\n                },\n                read_only=True,\n            )\n\n            class Meta:\n                model = Tag\n                exclude = ('id', 'content_type', 'object_id', )\n\n        serializer = TagSerializer(Tag.objects.all(), many=True, context={'request': request})\n        expected = [\n            {\n                'tagged_item': {\n                    'url': 'https://www.djangoproject.com/'\n                },\n                'tag': 'django'\n            },\n            {\n                'tagged_item': {\n                    'url': 'https://www.djangoproject.com/'\n                },\n                'tag': 'python'\n            },\n            {\n                'tagged_item': 'http://testserver/note/1/',\n                'tag': 'reminder'\n            }\n        ]\n        self.assertEqual(serializer.data, expected)\n\n    def test_invalid_model(self):\n        # Leaving out the Note model should result in a ValidationError\n        class TagSerializer(serializers.ModelSerializer):\n            tagged_item = GenericRelatedField({\n                Bookmark: BookmarkSerializer(),\n            }, read_only=True)\n\n            class Meta:\n                model = Tag\n                exclude = ('id', 'content_type', 'object_id', )\n        serializer = TagSerializer(Tag.objects.all(), many=True)\n\n        with self.assertRaises(serializers.ValidationError):\n            serializer.data\n\n    def test_relation_as_null(self):\n        class DetachableSerializer(serializers.ModelSerializer):\n            content_object = GenericRelatedField(\n                {\n                    Bookmark: serializers.HyperlinkedRelatedField(\n                        view_name='bookmark-detail',\n                        queryset=Bookmark.objects.all()),\n                    Note: serializers.HyperlinkedRelatedField(\n                        view_name='note-detail',\n                        queryset=Note.objects.all()),\n                },\n                read_only=True,\n            )\n\n            class Meta:\n                model = Detachable\n                exclude = ('id', 'content_type', 'object_id', )\n\n        serializer = DetachableSerializer(Detachable.objects.all(), many=True, context={'request': request})\n        expected = [\n            {\n                'content_object': 'http://testserver/note/1/',\n                'name': 'attached',\n            },\n            {\n                'content_object': None,\n                'name': 'detached',\n            }\n        ]\n        self.assertEqual(serializer.data, expected)\n\n    def test_deprecated_method_overridden(self):\n        with warnings.catch_warnings(record=True) as w:\n            class MyRelatedField(GenericRelatedField):\n                def determine_deserializer_for_data(self, value):\n                    return super(MyRelatedField, self).determine_deserializer_for_data(value)\n\n            self.assertEqual(len(w), 1)\n            self.assertIs(w[0].category, DeprecationWarning)\n\n    def test_deprecated_method_called(self):\n        f = GenericRelatedField({\n            Bookmark: serializers.HyperlinkedRelatedField(\n                view_name='bookmark-detail',\n                queryset=Bookmark.objects.all()),\n            Note: serializers.HyperlinkedRelatedField(\n                view_name='note-detail',\n                queryset=Note.objects.all()),\n        })\n        with warnings.catch_warnings(record=True) as w:\n            f.determine_deserializer_for_data(self.bookmark)\n\n            self.assertEqual(len(w), 1)\n            self.assertIs(w[0].category, DeprecationWarning)\n\n    def test_subclass_uses_registered_parent(self):\n        tagged_item = GenericRelatedField({\n            Note: NoteSerializer(),\n        }, read_only=True)\n\n        # NoteProxy instance should use the NoteSerializer,\n        # since no more specific serializer is registered\n        proxied = NoteProxy.objects.get(pk=self.note.pk)\n        serializer = tagged_item.get_serializer_for_instance(proxied)\n        self.assertIsInstance(serializer, NoteSerializer)\n\n    def test_subclass_uses_registered_subclass(self):\n        tagged_item = GenericRelatedField({\n            Note: NoteSerializer(),\n            NoteProxy: NoteProxySerializer(),\n        }, read_only=True)\n\n        # NoteProxy instance should use the NoteProxySerializer in\n        # preference to the NoteSerializer\n        proxied = NoteProxy.objects.get(pk=self.note.pk)\n        serializer = tagged_item.get_serializer_for_instance(proxied)\n        self.assertIsInstance(serializer, NoteProxySerializer)\n\n        # But Note instance should use the NoteSerializer\n        serializer = tagged_item.get_serializer_for_instance(self.note)\n        self.assertIsInstance(serializer, NoteSerializer)\n\n\n@override_settings(ROOT_URLCONF='generic_relations.tests.test_relations')\nclass TestGenericRelatedFieldDeserialization(TestCase):\n    def setUp(self):\n        self.bookmark = Bookmark.objects.create(\n            url='https://www.djangoproject.com/')\n        Tag.objects.create(tagged_item=self.bookmark, tag='django')\n        Tag.objects.create(tagged_item=self.bookmark, tag='python')\n        self.note = Note.objects.create(text='Remember the milk')\n\n    def test_hyperlink_serialization(self):\n        class TagSerializer(serializers.ModelSerializer):\n            tagged_item = GenericRelatedField(\n                {\n                    Bookmark: serializers.HyperlinkedRelatedField(\n                        view_name='bookmark-detail',\n                        queryset=Bookmark.objects.all()),\n                    Note: serializers.HyperlinkedRelatedField(\n                        view_name='note-detail',\n                        queryset=Note.objects.all()),\n                },\n                read_only=False,\n            )\n\n            class Meta:\n                model = Tag\n                exclude = ('id', 'content_type', 'object_id', )\n\n        serializer = TagSerializer(data={\n            'tag': 'reminder',\n            'tagged_item': reverse('note-detail', kwargs={'pk': self.note.pk})\n        }, context={'request': request})\n        serializer.is_valid(raise_exception=True)\n        expected = {\n            'tagged_item': 'http://testserver/note/1/',\n            'tag': 'reminder'\n        }\n        self.assertEqual(serializer.data, expected)\n\n    def test_configuration_error(self):\n        class TagSerializer(serializers.ModelSerializer):\n            tagged_item = GenericRelatedField(\n                {\n                    Bookmark: BookmarkSerializer(),\n                    Note: serializers.HyperlinkedRelatedField(\n                        view_name='note-detail',\n                        queryset=Note.objects.all()),\n                },\n                read_only=False,\n            )\n\n            class Meta:\n                model = Tag\n                exclude = ('id', 'content_type', 'object_id', )\n\n        serializer = TagSerializer(data={\n            'tag': 'reminder',\n            'tagged_item': 'just a string'\n        })\n\n        with self.assertRaises(ImproperlyConfigured):\n            tagged_item = serializer.fields['tagged_item']\n            tagged_item.get_deserializer_for_data('just a string')\n\n    def test_not_registered_view_name(self):\n        class TagSerializer(serializers.ModelSerializer):\n            tagged_item = GenericRelatedField(\n                {\n                    Bookmark: serializers.HyperlinkedRelatedField(\n                        view_name='bookmark-detail',\n                        queryset=Bookmark.objects.all()),\n                },\n                read_only=False\n            )\n\n            class Meta:\n                model = Tag\n                exclude = ('id', 'content_type', 'object_id', )\n\n        serializer = TagSerializer(data={\n            'tag': 'reminder',\n            'tagged_item': reverse('note-detail', kwargs={'pk': self.note.pk})\n        })\n        self.assertFalse(serializer.is_valid())\n\n    def test_invalid_url(self):\n        class TagSerializer(serializers.ModelSerializer):\n            tagged_item = GenericRelatedField(\n                {\n                    Bookmark: serializers.HyperlinkedRelatedField(\n                        view_name='bookmark-detail',\n                        queryset=Bookmark.objects.all()),\n                },\n                read_only=False,\n            )\n\n            class Meta:\n                model = Tag\n                exclude = ('id', 'content_type', 'object_id', )\n\n        serializer = TagSerializer(data={\n            'tag': 'reminder',\n            'tagged_item': 'foo-bar'\n        })\n\n        message = 'Could not determine a valid serializer for value %r.'\n        expected = {'tagged_item': {api_settings.NON_FIELD_ERRORS_KEY: message % 'foo-bar'}}\n\n        self.assertFalse(serializer.is_valid())\n        self.assertEqual(expected, serializer.errors)\n\n    def test_serializer_save(self):\n        class TagSerializer(serializers.ModelSerializer):\n            tagged_item = GenericRelatedField(\n                {\n                    Bookmark: serializers.HyperlinkedRelatedField(\n                        view_name='bookmark-detail',\n                        queryset=Bookmark.objects.all()),\n                    Note: serializers.HyperlinkedRelatedField(\n                        view_name='note-detail',\n                        queryset=Note.objects.all()),\n                },\n                read_only=False,\n            )\n\n            class Meta:\n                model = Tag\n                exclude = ('id', 'content_type', 'object_id', )\n\n        serializer = TagSerializer(data={\n            'tag': 'reminder',\n            'tagged_item': reverse('note-detail', kwargs={'pk': self.note.pk})\n        })\n        serializer.is_valid(raise_exception=True)\n        serializer.save()\n        tag = Tag.objects.get(pk=3)\n        self.assertEqual(tag.tagged_item, self.note)\n\n    def test_nullable_relation_serializer_save(self):\n        class DetachableSerializer(serializers.ModelSerializer):\n            content_object = GenericRelatedField(\n                {\n                    Bookmark: serializers.HyperlinkedRelatedField(\n                        view_name='bookmark-detail',\n                        queryset=Bookmark.objects.all()),\n                    Note: serializers.HyperlinkedRelatedField(\n                        view_name='note-detail',\n                        queryset=Note.objects.all()),\n                },\n                read_only=False,\n                required=False\n            )\n\n            class Meta:\n                model = Detachable\n                exclude = ('id', 'content_type', 'object_id', )\n\n        serializer = DetachableSerializer(data={'name': 'foo'})\n        serializer.is_valid(raise_exception=True)\n        serializer.save()\n        freeagent = Detachable.objects.get(pk=1)\n        self.assertEqual(freeagent.name, 'foo')\n        self.assertEqual(freeagent.content_object, None)\n\n    def test_deprecated_method_overridden(self):\n        with warnings.catch_warnings(record=True) as w:\n            class MyRelatedField(GenericRelatedField):\n                def determine_serializer_for_data(self, value):\n                    return super(MyRelatedField, self).determine_serializer_for_data(value)\n\n            self.assertEqual(len(w), 1)\n            self.assertIs(w[0].category, DeprecationWarning)\n\n    def test_deprecated_method_called(self):\n        f = GenericRelatedField({\n            Bookmark: serializers.HyperlinkedRelatedField(\n                view_name='bookmark-detail',\n                queryset=Bookmark.objects.all()),\n        })\n        with warnings.catch_warnings(record=True) as w:\n            f.determine_serializer_for_data('http://testserver/bookmark/1/')\n\n            self.assertEqual(len(w), 1)\n            self.assertIs(w[0].category, DeprecationWarning)\n\n\nclass TestGenericRelatedField(TestCase):\n    def test_multiple_declaration(self):\n        with self.assertRaises(RuntimeError):\n            class TagSerializer(serializers.ModelSerializer):\n                fields = {\n                    Bookmark: BookmarkSerializer(),\n                    Note: NoteSerializer(),\n                }\n                first = GenericRelatedField(fields)\n                second = GenericRelatedField(fields)\n\n                class Meta:\n                    model = Tag\n"
  },
  {
    "path": "generic_relations/tests/test_serializers.py",
    "content": "\n\nfrom django.test import TestCase\n\nfrom rest_framework import serializers\n\nfrom generic_relations.serializers import GenericModelSerializer\nfrom generic_relations.tests.models import Bookmark, Note\n\nfrom .test_relations import BookmarkSerializer, NoteSerializer\n\n\nclass TestGenericModelSerializer(TestCase):\n    def setUp(self):\n        self.bookmark = Bookmark.objects.create(\n            url='https://www.djangoproject.com/')\n        self.note = Note.objects.create(text='Remember the milk')\n        self.note2 = Note.objects.create(text='Reticulate the splines')\n\n        self.serializer = GenericModelSerializer(\n            {\n                Bookmark: BookmarkSerializer(),\n                Note: NoteSerializer(),\n            }\n        )\n        self.list_serializer = serializers.ListSerializer(child=self.serializer)\n\n    def test_serialize(self):\n        self.assertEqual(\n            self.serializer.to_representation(self.bookmark),\n            {'url': 'https://www.djangoproject.com/'},\n        )\n        self.assertEqual(\n            self.serializer.to_representation(self.note),\n            {'text': 'Remember the milk'},\n        )\n\n    def test_deserialize(self):\n        self.assertEqual(\n            self.serializer.to_internal_value({'url': 'https://www.djangoproject.com/'}),\n            {'url': 'https://www.djangoproject.com/'},\n        )\n        self.assertEqual(\n            self.serializer.to_internal_value({'text': 'Remember the milk'}),\n            {'text': 'Remember the milk'},\n        )\n\n    def test_serialize_list(self):\n        actual = self.list_serializer.to_representation([\n            self.bookmark, self.note, self.note2, self.bookmark,\n        ])\n        expected = [\n            {'url': 'https://www.djangoproject.com/'},\n            {'text': 'Remember the milk'},\n            {'text': 'Reticulate the splines'},\n            {'url': 'https://www.djangoproject.com/'},\n        ]\n        self.assertEqual(actual, expected)\n\n    def test_deserialize_list(self):\n        validated_data = self.list_serializer.to_internal_value([\n            {'url': 'https://www.djangoproject.com/'},\n            {'text': 'Remember the milk'},\n            {'text': 'Reticulate the splines'},\n            {'url': 'https://www.djangoproject.com/'},\n        ])\n\n        self.assertEqual(validated_data, [\n            {'url': 'https://www.djangoproject.com/'},\n            {'text': 'Remember the milk'},\n            {'text': 'Reticulate the splines'},\n            {'url': 'https://www.djangoproject.com/'},\n        ])\n\n    def test_is_valid_raise_exception(self):\n        serializer = GenericModelSerializer(\n            serializers={Bookmark: BookmarkSerializer()},\n            data={'url': 'not-a-url'},\n            )\n\n        with self.assertRaises(serializers.ValidationError):\n            serializer.is_valid(raise_exception=True)\n"
  },
  {
    "path": "manage.py",
    "content": "#!/usr/bin/env python\nimport os\nimport sys\n\nif __name__ == \"__main__\":\n    os.environ.setdefault(\"DJANGO_SETTINGS_MODULE\", \"testsettings\")\n\n    from django.core.management import execute_from_command_line\n\n    execute_from_command_line(sys.argv)\n"
  },
  {
    "path": "setup.cfg",
    "content": "[bdist_wheel]\nuniversal = 1\n"
  },
  {
    "path": "setup.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\nfrom setuptools import setup, find_packages\nfrom os.path import abspath, dirname, join\n\n\ndef read_relative_file(filename):\n    \"\"\"\n    Returns contents of the given file, whose path is supposed relative\n    to this module.\n    \"\"\"\n    with open(join(dirname(abspath(__file__)), filename), \"r\") as f:\n        return f.read()\n\n\nsetup(\n    name=\"rest-framework-generic-relations\",\n    version=\"2.1.0\",\n    url=\"https://github.com/Ian-Foote/rest-framework-generic-relations\",\n    license=\"BSD\",\n    description=\"Generic Relations for Django Rest Framework\",\n    long_description=read_relative_file(\"README.md\"),\n    long_description_content_type=\"text/markdown\",\n    author=\"Ian Foote\",\n    author_email=\"python@ian.feete.org\",\n    packages=find_packages(),\n    include_package_data=True,\n    install_requires=[\"djangorestframework>=3.11.0\"],\n    python_requires=\">=3.6\",\n    classifiers=[\n        \"Environment :: Web Environment\",\n        \"Framework :: Django\",\n        \"Intended Audience :: Developers\",\n        \"Operating System :: OS Independent\",\n        \"Programming Language :: Python\",\n        \"Programming Language :: Python :: 3.6\",\n        \"Programming Language :: Python :: 3.7\",\n        \"Programming Language :: Python :: 3.8\",\n        \"Programming Language :: Python :: 3.9\",\n        \"Programming Language :: Python :: 3.10\",\n        \"Programming Language :: Python :: 3.11\",\n        \"Programming Language :: Python :: Implementation :: CPython\",\n        \"Programming Language :: Python :: Implementation :: PyPy\",\n        \"Framework :: Django :: 2.2\",\n        \"Framework :: Django :: 3.0\",\n        \"Framework :: Django :: 3.1\",\n        \"Framework :: Django :: 3.2\",\n        \"Framework :: Django :: 4.0\",\n        \"Framework :: Django :: 4.1\",\n    ],\n)\n"
  },
  {
    "path": "testsettings.py",
    "content": "DATABASES = {\n    'default': {\n        'ENGINE': 'django.db.backends.sqlite3',\n        'NAME': ':memory:',\n    },\n}\n\nINSTALLED_APPS = (\n    'django.contrib.contenttypes',\n\n    'generic_relations',\n    'generic_relations.tests',\n)\n\nROOT_URLCONF = ''\n\nSECRET_KEY = 'abcde12345'\n\nMIDDLEWARE_CLASSES = (\n    'django.middleware.common.CommonMiddleware',\n    'django.middleware.csrf.CsrfViewMiddleware'\n)\n"
  },
  {
    "path": "tox.ini",
    "content": "[tox]\nenvlist =\n    # note: min/max python versions specified here;\n    # testing in-between versions here seems a waste of resources.\n    {py37,py39}-{dj22,dj31,dj32}-{drf311,drf312}\n    py311-{dj32,dj41}-{drf313,drf314}\n[testenv]\nchangedir = {toxinidir}\ncommands = pytest --ds=testsettings {posargs}\ndeps =\n    pytest-django\n    dj22: Django~=2.2.17\n    dj31: Django~=3.1.0\n    dj32: Django~=3.2.0\n    dj41: Django~=4.1.0\n    drf311: djangorestframework~=3.11.0\n    drf312: djangorestframework~=3.12.0\n    drf313: djangorestframework~=3.13.0\n    drf314: djangorestframework~=3.14.0\n"
  }
]