diff --git a/taggit/admin.py b/taggit/admin.py index a9339cfb..7e98f3f3 100644 --- a/taggit/admin.py +++ b/taggit/admin.py @@ -10,7 +10,7 @@ class TaggedItemInline(admin.StackedInline): @admin.register(Tag) class TagAdmin(admin.ModelAdmin): inlines = [TaggedItemInline] - list_display = ["name", "slug"] - ordering = ["name", "slug"] + list_display = ["name", "slug", "language_code"] + ordering = ["name", "slug", "language_code"] search_fields = ["name"] prepopulated_fields = {"slug": ["name"]} diff --git a/taggit/forms.py b/taggit/forms.py index fbe62d21..ebfe2ec1 100644 --- a/taggit/forms.py +++ b/taggit/forms.py @@ -1,6 +1,7 @@ +import json + from django import forms from django.utils.translation import gettext as _ - from taggit.utils import edit_string_for_tags, parse_tags @@ -24,8 +25,13 @@ class TagField(forms.CharField): def clean(self, value): value = super().clean(value) + if not value: + return {'language_code': '', 'tags': []} + value_obj = json.loads(value) + tags_str = parse_tags(value_obj['tags']) + value_obj['tags'] = tags_str try: - return parse_tags(value) + return value_obj except ValueError: raise forms.ValidationError( _("Please provide a comma-separated list of tags.") diff --git a/taggit/managers.py b/taggit/managers.py index bf95921c..89d57c19 100644 --- a/taggit/managers.py +++ b/taggit/managers.py @@ -124,10 +124,10 @@ def _lookup_kwargs(self): return self.through.lookup_kwargs(self.instance) @require_instance_manager - def add(self, *tags): + def add(self, lang, tags): db = router.db_for_write(self.through, instance=self.instance) - tag_objs = self._to_tag_model_instances(tags) + tag_objs = self._to_tag_model_instances(lang, tags) new_ids = {t.pk for t in tag_objs} # NOTE: can we hardcode 'tag_id' here or should the column name be got @@ -165,7 +165,7 @@ def add(self, *tags): using=db, ) - def _to_tag_model_instances(self, tags): + def _to_tag_model_instances(self, lang, tags): """ Takes an iterable containing either strings, tag objects, or a mixture of both and returns set of tag objects. @@ -189,36 +189,19 @@ def _to_tag_model_instances(self, tags): case_insensitive = getattr(settings, "TAGGIT_CASE_INSENSITIVE", False) manager = self.through.tag_model()._default_manager.using(db) + existing = [] - if case_insensitive: - # Some databases can do case-insensitive comparison with IN, which - # would be faster, but we can't rely on it or easily detect it. - existing = [] - tags_to_create = [] - - for name in str_tags: - try: - tag = manager.get(name__iexact=name) - existing.append(tag) - except self.through.tag_model().DoesNotExist: - tags_to_create.append(name) - else: - # If str_tags has 0 elements Django actually optimizes that to not - # do a query. Malcolm is very smart. - existing = manager.filter(name__in=str_tags) - tags_to_create = str_tags - {t.name for t in existing} - - tag_objs.update(existing) - - for new_tag in tags_to_create: + # Some databases can do case-insensitive comparison with IN, which + # would be faster, but we can't rely on it or easily detect it. + for tag in str_tags: if case_insensitive: - tag, created = manager.get_or_create( - name__iexact=new_tag, defaults={"name": new_tag} - ) + _tag = manager.filter(name__iexact=tag, language_code=lang).first() else: - tag, created = manager.get_or_create(name=new_tag) + _tag = manager.filter(name=tag, language_code=lang).first() + if _tag: + existing.append(_tag) - tag_objs.add(tag) + tag_objs.update(existing) return tag_objs @@ -231,7 +214,7 @@ def slugs(self): return self.get_queryset().values_list("slug", flat=True) @require_instance_manager - def set(self, *tags, **kwargs): + def set(self, tags_obj, **kwargs): """ Set the object's tags to the given n tags. If the clear kwarg is True then all existing tags are removed (using `.clear()`) and the new tags @@ -240,30 +223,31 @@ def set(self, *tags, **kwargs): """ db = router.db_for_write(self.through, instance=self.instance) clear = kwargs.pop("clear", False) - + tags = tags_obj['tags'] + lang = tags_obj['language_code'] if clear: self.clear() - self.add(*tags) + self.add(lang, tags) else: # make sure we're working with a collection of a uniform type - objs = self._to_tag_model_instances(tags) + objs = self._to_tag_model_instances(lang, tags) # get the existing tag strings old_tag_strs = set( self.through._default_manager.using(db) - .filter(**self._lookup_kwargs()) - .values_list("tag__name", flat=True) + .filter(**self._lookup_kwargs(), tag__language_code=lang) + .values_list("tag__id", flat=True) ) new_objs = [] for obj in objs: - if obj.name in old_tag_strs: - old_tag_strs.remove(obj.name) + if obj.id in old_tag_strs: + old_tag_strs.remove(obj.id) else: new_objs.append(obj) self.remove(*old_tag_strs) - self.add(*new_objs) + self.add(lang, new_objs) @require_instance_manager def remove(self, *tags): @@ -275,7 +259,7 @@ def remove(self, *tags): qs = ( self.through._default_manager.using(db) .filter(**self._lookup_kwargs()) - .filter(tag__name__in=tags) + .filter(tag__id__in=tags) ) old_ids = set(qs.values_list("tag_id", flat=True)) @@ -514,7 +498,7 @@ def post_through_setup(self, cls): ) def save_form_data(self, instance, value): - getattr(instance, self.name).set(*value) + getattr(instance, self.name).set(value) def formfield(self, form_class=TagField, **kwargs): defaults = { diff --git a/taggit/migrations/0004_auto_20200204_1036.py b/taggit/migrations/0004_auto_20200204_1036.py new file mode 100644 index 00000000..485a40dd --- /dev/null +++ b/taggit/migrations/0004_auto_20200204_1036.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.18 on 2020-02-04 09:36 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('taggit', '0003_taggeditem_add_unique_index'), + ] + + operations = [ + migrations.AddField( + model_name='tag', + name='language_code', + field=models.CharField(default='sk', max_length=255, verbose_name='Language code'), + ), + migrations.AlterField( + model_name='tag', + name='name', + field=models.CharField(max_length=100, verbose_name='Name'), + ), + migrations.AlterField( + model_name='tag', + name='slug', + field=models.SlugField(max_length=100, verbose_name='Slug'), + ), + ] diff --git a/taggit/migrations/0005_auto_20200204_1049.py b/taggit/migrations/0005_auto_20200204_1049.py new file mode 100644 index 00000000..37ccc8ea --- /dev/null +++ b/taggit/migrations/0005_auto_20200204_1049.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.18 on 2020-02-04 09:49 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('taggit', '0004_auto_20200204_1036'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='tag', + unique_together=set([('slug', 'language_code'), ('name', 'language_code')]), + ), + ] diff --git a/taggit/migrations/0006_alter_tag_language_code_and_more.py b/taggit/migrations/0006_alter_tag_language_code_and_more.py new file mode 100644 index 00000000..1825cb70 --- /dev/null +++ b/taggit/migrations/0006_alter_tag_language_code_and_more.py @@ -0,0 +1,29 @@ +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('taggit', '0005_auto_20200204_1049'), + ] + + operations = [ + migrations.AlterField( + model_name='tag', + name='language_code', + field=models.CharField(choices=list(map(lambda cou: (cou['code'], cou['code']), settings.PARLER_LANGUAGES[1])), default=settings.LANGUAGE_CODE, max_length=255, verbose_name='Language code'), + ), + migrations.AlterField( + model_name='taggeditem', + name='content_type', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_tagged_items', to='contenttypes.contenttype', verbose_name='Content type'), + ), + migrations.AlterField( + model_name='taggeditem', + name='tag', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_items', to='taggit.tag'), + ), + ] diff --git a/taggit/models.py b/taggit/models.py index 124cd59d..9e018d2d 100644 --- a/taggit/models.py +++ b/taggit/models.py @@ -3,6 +3,7 @@ from django.db import IntegrityError, models, router, transaction from django.utils.text import slugify from django.utils.translation import gettext, gettext_lazy as _ +from django.conf import settings try: from unidecode import unidecode @@ -13,8 +14,9 @@ def unidecode(tag): class TagBase(models.Model): - name = models.CharField(verbose_name=_("Name"), unique=True, max_length=100) - slug = models.SlugField(verbose_name=_("Slug"), unique=True, max_length=100) + name = models.CharField(verbose_name=_("Name"), max_length=100) + slug = models.SlugField(verbose_name=_("Slug"), max_length=100) + language_code = models.CharField(verbose_name=_('Language code'), max_length=255, default=settings.LANGUAGE_CODE, choices=list(map(lambda cou: (cou['code'], cou['code']), settings.PARLER_LANGUAGES[1]))) def __str__(self): return self.name @@ -76,6 +78,7 @@ class Meta: verbose_name = _("Tag") verbose_name_plural = _("Tags") app_label = "taggit" + unique_together = (("name", "language_code",), ("slug", "language_code",),) class ItemBase(models.Model): diff --git a/taggit/urls.py b/taggit/urls.py new file mode 100644 index 00000000..0b649631 --- /dev/null +++ b/taggit/urls.py @@ -0,0 +1,10 @@ +from django.conf.urls import url + +from .views import TagAutocomplete + +urlpatterns = [ + url(r'^tag-autocomplete/$', TagAutocomplete.as_view(), name='tag-autocomplete', ), +] + + + diff --git a/taggit/views.py b/taggit/views.py index 5a43de0e..18f28e76 100644 --- a/taggit/views.py +++ b/taggit/views.py @@ -1,4 +1,6 @@ +from dal import autocomplete from django.contrib.contenttypes.models import ContentType +from django.db.models import Q from django.shortcuts import get_object_or_404 from django.views.generic.list import ListView @@ -44,3 +46,18 @@ def get_context_data(self, **kwargs): context["extra_context"] = {} context["extra_context"]["tag"] = self.tag return context + + +class TagAutocomplete(autocomplete.Select2QuerySetView): + def get_queryset(self): + # Don't forget to filter out results depending on the visitor ! + if not self.request.user.is_authenticated() or ( + not self.request.user.is_superuser and not self.request.user.has_perm('taggit.change_tag')): + return Tag.objects.none() + + qs = Tag.objects.all() + + if self.q: + qs = Tag.objects.filter( + Q(name__icontains=self.q) | Q(slug__icontains=self.q)).all() + return qs