diff --git a/bootcamp/articles/migrations/0002_auto_20200626_1410.py b/bootcamp/articles/migrations/0002_auto_20200626_1410.py new file mode 100755 index 000000000..8357e7d8e --- /dev/null +++ b/bootcamp/articles/migrations/0002_auto_20200626_1410.py @@ -0,0 +1,43 @@ +# Generated by Django 3.0.7 on 2020-06-26 14:10 + +from django.db import migrations, models +import django.db.models.deletion +import taggit_selectize.managers + + +def recreate_tagged_items(apps, schema_editor): + TaggedArticle = apps.get_model('articles', 'TaggedArticle') + TaggedItem = apps.get_model('taggit', 'TaggedItem') + + old_tagged_items = TaggedItem.objects.filter(content_type__model='article') + for old in old_tagged_items: + TaggedArticle.objects.create(tag=old.tag, content_object_id=old.object_id) + old_tagged_items.delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('taggit', '0003_taggeditem_add_unique_index'), + ('articles', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='TaggedArticle', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('content_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='articles.Article')), + ('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='articles_taggedarticle_items', to='taggit.Tag')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AlterField( + model_name='article', + name='tags', + field=taggit_selectize.managers.TaggableManager(help_text='A comma-separated list of tags.', through='articles.TaggedArticle', to='taggit.Tag', verbose_name='Tags'), + ), + migrations.RunPython(recreate_tagged_items, reverse_code=migrations.RunPython.noop), + ] diff --git a/bootcamp/articles/models.py b/bootcamp/articles/models.py index 44fdb22d9..d484ca56b 100755 --- a/bootcamp/articles/models.py +++ b/bootcamp/articles/models.py @@ -1,6 +1,6 @@ from django.conf import settings from django.db import models -from django.db.models import Count +from django.db.models import Count, F from django.utils.translation import ugettext_lazy as _ from slugify import slugify @@ -8,8 +8,8 @@ from django_comments.signals import comment_was_posted from markdownx.models import MarkdownxField from markdownx.utils import markdownify -from taggit.managers import TaggableManager - +from taggit_selectize.managers import TaggableManager +from taggit.models import TaggedItemBase from bootcamp.notifications.models import Notification, notification_handler @@ -25,20 +25,14 @@ def get_drafts(self): """Returns only the items marked as DRAFT in the current queryset.""" return self.filter(status="D") - def get_counted_tags(self): - tag_dict = {} - query = ( - self.filter(status="P").annotate(tagged=Count("tags")).filter(tags__gt=0) - ) - for obj in query: - for tag in obj.tags.names(): - if tag not in tag_dict: - tag_dict[tag] = 1 + @staticmethod + def get_counted_tags(): + return TaggedArticle.objects.filter(content_object__status='P').order_by('tag__id').\ + annotate(name=F('tag__name'), slug=F('tag__slug'),).values('slug', 'name').annotate(count=Count('tag')) - else: # pragma: no cover - tag_dict[tag] += 1 - return tag_dict.items() +class TaggedArticle(TaggedItemBase): + content_object = models.ForeignKey('Article', on_delete=models.CASCADE) class Article(models.Model): @@ -61,7 +55,7 @@ class Article(models.Model): status = models.CharField(max_length=1, choices=STATUS, default=DRAFT) content = MarkdownxField() edited = models.BooleanField(default=False) - tags = TaggableManager() + tags = TaggableManager(through=TaggedArticle) objects = ArticleQuerySet.as_manager() class Meta: diff --git a/bootcamp/articles/urls.py b/bootcamp/articles/urls.py index 74a286506..a084c4829 100755 --- a/bootcamp/articles/urls.py +++ b/bootcamp/articles/urls.py @@ -2,6 +2,7 @@ from bootcamp.articles.views import ( ArticlesListView, + ArticlesByTagListView, DraftsListView, CreateArticleView, EditArticleView, @@ -11,6 +12,7 @@ app_name = "articles" urlpatterns = [ url(r"^$", ArticlesListView.as_view(), name="list"), + url(r"^tag/(?P[-\w]+)/$", ArticlesByTagListView.as_view(), name="by_tag_list"), url(r"^write-new-article/$", CreateArticleView.as_view(), name="write_new"), url(r"^drafts/$", DraftsListView.as_view(), name="drafts"), url(r"^edit/(?P\d+)/$", EditArticleView.as_view(), name="edit_article"), diff --git a/bootcamp/articles/views.py b/bootcamp/articles/views.py index c7013c717..c958ec2ec 100755 --- a/bootcamp/articles/views.py +++ b/bootcamp/articles/views.py @@ -5,9 +5,11 @@ from django.utils.translation import ugettext_lazy as _ from bootcamp.helpers import AuthorRequiredMixin -from bootcamp.articles.models import Article +from bootcamp.articles.models import Article, TaggedArticle from bootcamp.articles.forms import ArticleForm +from taggit.views import TagListMixin + class ArticlesListView(LoginRequiredMixin, ListView): """Basic ListView implementation to call the published articles list.""" @@ -25,6 +27,20 @@ def get_queryset(self, **kwargs): return Article.objects.get_published() +class ArticlesByTagListView(TagListMixin, ArticlesListView): + template_name = 'articles/article_list.html' + + def get_queryset(self, **kwargs): + qs = Article.objects.filter( + pk__in=TaggedArticle.objects.filter(tag=self.tag).values_list("content_object_id", flat=True)) + return qs.get_published() + + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) + context["filtered_by_tag"] = self.tag + return context + + class DraftsListView(ArticlesListView): """Overriding the original implementation to call the drafts articles list.""" diff --git a/bootcamp/templates/articles/article_create.html b/bootcamp/templates/articles/article_create.html index e6f815bcd..7daf60e60 100755 --- a/bootcamp/templates/articles/article_create.html +++ b/bootcamp/templates/articles/article_create.html @@ -3,6 +3,8 @@ {% load crispy_forms_tags %} {% block head %} + {# Load Jquery in head for keep taggit_serializer working #} + {% endblock head %} {% block content %} diff --git a/bootcamp/templates/articles/article_list.html b/bootcamp/templates/articles/article_list.html index bc4e67540..55c07fac0 100755 --- a/bootcamp/templates/articles/article_list.html +++ b/bootcamp/templates/articles/article_list.html @@ -16,6 +16,9 @@
@@ -38,8 +41,8 @@

{{ article.title|title }}

{% trans 'Posted' %} {{ article.timestamp|naturaltime }} {{ article.user.get_profile_name|title }} - {% for tag in article.tags.names %} - {{ tag }} + {% for tag in article.tags.all %} + {{ tag.name }} {% endfor %}
@@ -86,8 +89,8 @@

{% trans 'There is no published article yet' %}.
{% trans 'Cloud tag' %}
diff --git a/bootcamp/templates/articles/article_update.html b/bootcamp/templates/articles/article_update.html index 480cc06f9..460c4ab82 100755 --- a/bootcamp/templates/articles/article_update.html +++ b/bootcamp/templates/articles/article_update.html @@ -3,6 +3,8 @@ {% load crispy_forms_tags %} {% block head %} + {# Load Jquery in head for keep taggit_serializer working #} + {% endblock head %} {% block content %} diff --git a/config/settings/base.py b/config/settings/base.py index ecad77e50..5a5cb31bc 100755 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -79,6 +79,7 @@ "graphene_django", "markdownx", "taggit", + "taggit_selectize", ] LOCAL_APPS = [ "bootcamp.users.apps.UsersConfig", @@ -259,3 +260,7 @@ # GraphQL settings GRAPHENE = {"SCHEMA": "config.schema.schema"} + +# taggit-selectize settings +TAGGIT_TAGS_FROM_STRING = 'taggit_selectize.utils.parse_tags' +TAGGIT_STRING_FROM_TAGS = 'taggit_selectize.utils.join_tags' diff --git a/config/urls.py b/config/urls.py index 1787dfb9a..6c13cafbd 100755 --- a/config/urls.py +++ b/config/urls.py @@ -33,6 +33,8 @@ url(r"^messages/", include("bootcamp.messager.urls", namespace="messager")), url(r"^qa/", include("bootcamp.qa.urls", namespace="qa")), url(r"^search/", include("bootcamp.search.urls", namespace="search")), + url(r'^taggit/', include('taggit_selectize.urls')), + ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) if settings.DEBUG: diff --git a/requirements/base.txt b/requirements/base.txt index a106ca342..5e5c60d50 100755 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -18,6 +18,7 @@ django-markdownx # https://github.com/neutronX/django-markdownx django-redis # https://github.com/niwinz/django-redis django-taggit # https://github.com/alex/django-taggit sorl-thumbnail # https://github.com/jazzband/sorl-thumbnail +taggit-selectize # https://github.com/chhantyal/taggit-selectize # Channels # ------------------------------------------------------------------------------