Skip to content

Commit

Permalink
Merge pull request #738 from jazzband/serializer-vendoring
Browse files Browse the repository at this point in the history
Vendor in `django-taggit-serializer`
  • Loading branch information
rtpg committed Jun 29, 2021
2 parents 5c9e448 + e796d50 commit bd342ab
Show file tree
Hide file tree
Showing 10 changed files with 368 additions and 3 deletions.
3 changes: 1 addition & 2 deletions CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,8 @@ To do work on the docs, proceed with the following steps:

.. code-block:: console
cd docs
pip install sphinx
make html
sphinx-build -n -W docs docs/_build
Send pull request
-----------------
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright (c) Alex Gaynor and individual contributors.
Copyright (c) Alex Gaynor, Paul Oostenrijk, and individual contributors.
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ tagging to your project easy and fun.
getting_started
forms
admin
serializers
api
faq
custom_tagging
Expand Down
20 changes: 20 additions & 0 deletions docs/serializers.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Usage With Django Rest Framework
================================

Because the tags in `django-taggit` need to be added into a `TaggableManager()` we cannot use the usual `Serializer` that we get from Django REST Framework. Because this is trying to save the tags into a `list`, which will throw an exception.

To accept tags through a `REST` API call we need to add the following to our `Serializer`::


from taggit.serializers import (TagListSerializerField,
TaggitSerializer)


class YourSerializer(TaggitSerializer, serializers.ModelSerializer):

tags = TagListSerializerField()

class Meta:
model = YourModel

And you're done, so now you can add tags to your model.
123 changes: 123 additions & 0 deletions taggit/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"""
Django-taggit serializer support
Originally vendored from https://github.com/glemmaPaul/django-taggit-serializer
"""
import json

# Third party
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers


class TagList(list):
def __init__(self, *args, **kwargs):
pretty_print = kwargs.pop("pretty_print", True)
list.__init__(self, *args, **kwargs)
self.pretty_print = pretty_print

def __add__(self, rhs):
return TagList(list.__add__(self, rhs))

def __getitem__(self, item):
result = list.__getitem__(self, item)
try:
return TagList(result)
except TypeError:
return result

def __str__(self):
if self.pretty_print:
return json.dumps(self, sort_keys=True, indent=4, separators=(",", ": "))
else:
return json.dumps(self)


class TagListSerializerField(serializers.Field):
child = serializers.CharField()
default_error_messages = {
"not_a_list": _('Expected a list of items but got type "{input_type}".'),
"invalid_json": _(
"Invalid json list. A tag list submitted in string"
" form must be valid json."
),
"not_a_str": _("All list items must be of string type."),
}
order_by = None

def __init__(self, **kwargs):
pretty_print = kwargs.pop("pretty_print", True)

style = kwargs.pop("style", {})
kwargs["style"] = {"base_template": "textarea.html"}
kwargs["style"].update(style)

super(TagListSerializerField, self).__init__(**kwargs)

self.pretty_print = pretty_print

def to_internal_value(self, value):
if isinstance(value, str):
if not value:
value = "[]"
try:
value = json.loads(value)
except ValueError:
self.fail("invalid_json")

if not isinstance(value, list):
self.fail("not_a_list", input_type=type(value).__name__)

for s in value:
if not isinstance(s, str):
self.fail("not_a_str")

self.child.run_validation(s)

return value

def to_representation(self, value):
if not isinstance(value, TagList):
if not isinstance(value, list):
if self.order_by:
tags = value.all().order_by(*self.order_by)
else:
tags = value.all()
value = [tag.name for tag in tags]
value = TagList(value, pretty_print=self.pretty_print)

return value


class TaggitSerializer(serializers.Serializer):
def create(self, validated_data):
to_be_tagged, validated_data = self._pop_tags(validated_data)

tag_object = super(TaggitSerializer, self).create(validated_data)

return self._save_tags(tag_object, to_be_tagged)

def update(self, instance, validated_data):
to_be_tagged, validated_data = self._pop_tags(validated_data)

tag_object = super(TaggitSerializer, self).update(instance, validated_data)

return self._save_tags(tag_object, to_be_tagged)

def _save_tags(self, tag_object, tags):
for key in tags.keys():
tag_values = tags.get(key)
getattr(tag_object, key).set(*tag_values)

return tag_object

def _pop_tags(self, validated_data):
to_be_tagged = {}

for key in self.fields.keys():
field = self.fields[key]
if isinstance(field, TagListSerializerField):
if key in validated_data:
to_be_tagged[key] = validated_data.pop(key)

return (to_be_tagged, validated_data)
126 changes: 126 additions & 0 deletions tests/migrations/0004_auto_20210619_0826.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Generated by Django 3.2.4 on 2021-06-19 08:26

import django.db.models.deletion
from django.db import migrations, models

import taggit.managers


class Migration(migrations.Migration):

dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("taggit", "0003_taggeditem_add_unique_index"),
("tests", "0003_auto_20210310_0918"),
]

operations = [
migrations.AlterField(
model_name="officialtag",
name="name",
field=models.CharField(max_length=100, unique=True, verbose_name="name"),
),
migrations.AlterField(
model_name="officialtag",
name="slug",
field=models.SlugField(max_length=100, unique=True, verbose_name="slug"),
),
migrations.AlterField(
model_name="officialthroughmodel",
name="content_type",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="tests_officialthroughmodel_tagged_items",
to="contenttypes.contenttype",
verbose_name="content type",
),
),
migrations.AlterField(
model_name="officialthroughmodel",
name="object_id",
field=models.IntegerField(db_index=True, verbose_name="object ID"),
),
migrations.AlterField(
model_name="taggedcustompk",
name="content_type",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="tests_taggedcustompk_tagged_items",
to="contenttypes.contenttype",
verbose_name="content type",
),
),
migrations.AlterField(
model_name="throughgfk",
name="content_type",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="tests_throughgfk_tagged_items",
to="contenttypes.contenttype",
verbose_name="content type",
),
),
migrations.AlterField(
model_name="throughgfk",
name="object_id",
field=models.IntegerField(db_index=True, verbose_name="object ID"),
),
migrations.AlterField(
model_name="trackedtag",
name="name",
field=models.CharField(max_length=100, unique=True, verbose_name="name"),
),
migrations.AlterField(
model_name="trackedtag",
name="slug",
field=models.SlugField(max_length=100, unique=True, verbose_name="slug"),
),
migrations.AlterField(
model_name="uuidtag",
name="name",
field=models.CharField(max_length=100, unique=True, verbose_name="name"),
),
migrations.AlterField(
model_name="uuidtag",
name="slug",
field=models.SlugField(max_length=100, unique=True, verbose_name="slug"),
),
migrations.AlterField(
model_name="uuidtaggeditem",
name="content_type",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="tests_uuidtaggeditem_tagged_items",
to="contenttypes.contenttype",
verbose_name="content type",
),
),
migrations.AlterField(
model_name="uuidtaggeditem",
name="object_id",
field=models.UUIDField(db_index=True, verbose_name="object ID"),
),
migrations.CreateModel(
name="TestModel",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"tags",
taggit.managers.TaggableManager(
help_text="A comma-separated list of tags.",
through="taggit.TaggedItem",
to="taggit.Tag",
verbose_name="Tags",
),
),
],
),
]
5 changes: 5 additions & 0 deletions tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
)


# base test model
class TestModel(models.Model):
tags = TaggableManager()


# Ensure that two TaggableManagers with custom through model are allowed.
class Through1(TaggedItemBase):
content_object = models.ForeignKey("MultipleTags", on_delete=models.CASCADE)
Expand Down
16 changes: 16 additions & 0 deletions tests/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import django
from rest_framework import serializers

from taggit.serializers import TaggitSerializer, TagListSerializerField

from .models import TestModel


class TestModelSerializer(TaggitSerializer, serializers.ModelSerializer):

tags = TagListSerializerField()

class Meta:
model = TestModel
if django.VERSION >= (1, 11):
fields = "__all__"

0 comments on commit bd342ab

Please sign in to comment.