From 26cc6940083bd385fc189dd825adbe9068ac5d6c Mon Sep 17 00:00:00 2001 From: Gabriel Augendre Date: Wed, 3 Mar 2021 17:30:38 +0100 Subject: [PATCH] Replace keywords by a Tag model --- articles/admin.py | 15 +++-- .../migrations/0027_auto_20210303_1633.py | 62 +++++++++++++++++++ .../0028_remove_article_keywords.py | 17 +++++ .../migrations/0029_auto_20210303_1711.py | 17 +++++ articles/models.py | 28 +++++---- articles/views/html.py | 4 +- 6 files changed, 123 insertions(+), 20 deletions(-) create mode 100644 articles/migrations/0027_auto_20210303_1633.py create mode 100644 articles/migrations/0028_remove_article_keywords.py create mode 100644 articles/migrations/0029_auto_20210303_1711.py diff --git a/articles/admin.py b/articles/admin.py index 57a57f4..ffcebe8 100644 --- a/articles/admin.py +++ b/articles/admin.py @@ -1,11 +1,9 @@ -import copy - from django.contrib import admin, messages from django.contrib.admin import register from django.contrib.auth.admin import UserAdmin from django.shortcuts import redirect -from .models import Article, User +from .models import Article, Tag, User admin.site.register(User, UserAdmin) @@ -40,7 +38,7 @@ class ArticleAdmin(admin.ModelAdmin): { "fields": [ ("title", "slug"), - ("author", "keywords"), + ("author", "tags"), ("status", "published_at"), ("created_at", "updated_at"), ("views_count", "read_time"), @@ -71,7 +69,8 @@ class ArticleAdmin(admin.ModelAdmin): ] prepopulated_fields = {"slug": ("title",)} change_form_template = "articles/article_change_form.html" - search_fields = ["title", "content"] + search_fields = ["title", "content", "tags__name"] + autocomplete_fields = ["tags"] def publish(self, request, queryset): if not request.user.has_perm("articles.change_article"): @@ -133,3 +132,9 @@ class ArticleAdmin(admin.ModelAdmin): return bool(instance.custom_css) has_custom_css.boolean = True + + +@register(Tag) +class TagAdmin(admin.ModelAdmin): + list_display = ["name"] + search_fields = ["name"] diff --git a/articles/migrations/0027_auto_20210303_1633.py b/articles/migrations/0027_auto_20210303_1633.py new file mode 100644 index 0000000..b950f18 --- /dev/null +++ b/articles/migrations/0027_auto_20210303_1633.py @@ -0,0 +1,62 @@ +# Generated by Django 3.1.5 on 2021-03-03 15:33 + +from django.db import migrations, models + + +def forwards(apps, schema_editor): + Tag = apps.get_model("articles", "Tag") + Article = apps.get_model("articles", "Article") + db_alias = schema_editor.connection.alias + articles = Article.objects.using(db_alias).all() + for article in articles: + tags = [] + for keyword in list( + filter(None, map(lambda k: k.strip(), article.keywords.split(","))) + ): + tag = Tag.objects.using(db_alias).filter(name__iexact=keyword).first() + if tag is None: + tag = Tag.objects.create(name=keyword) + tags.append(tag) + article.tags.set(tags) + article.keywords = "" + Article.objects.bulk_update(articles, ["keywords"]) + + +def backwards(apps, schema_editor): + Article = apps.get_model("articles", "Article") + db_alias = schema_editor.connection.alias + articles = Article.objects.using(db_alias).all() + for article in articles: + article.keywords = ",".join(map(lambda tag: tag.name, article.tags.all())) + Article.objects.bulk_update(articles, ["keywords"]) + + +class Migration(migrations.Migration): + + dependencies = [ + ("articles", "0026_article_draft_key"), + ] + + operations = [ + migrations.CreateModel( + name="Tag", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255, unique=True)), + ], + ), + migrations.AddField( + model_name="article", + name="tags", + field=models.ManyToManyField(related_name="articles", to="articles.Tag"), + ), + migrations.RunPython(forwards, backwards), + ] diff --git a/articles/migrations/0028_remove_article_keywords.py b/articles/migrations/0028_remove_article_keywords.py new file mode 100644 index 0000000..fb69e05 --- /dev/null +++ b/articles/migrations/0028_remove_article_keywords.py @@ -0,0 +1,17 @@ +# Generated by Django 3.1.5 on 2021-03-03 15:52 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("articles", "0027_auto_20210303_1633"), + ] + + operations = [ + migrations.RemoveField( + model_name="article", + name="keywords", + ), + ] diff --git a/articles/migrations/0029_auto_20210303_1711.py b/articles/migrations/0029_auto_20210303_1711.py new file mode 100644 index 0000000..45819c1 --- /dev/null +++ b/articles/migrations/0029_auto_20210303_1711.py @@ -0,0 +1,17 @@ +# Generated by Django 3.1.5 on 2021-03-03 16:11 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("articles", "0028_remove_article_keywords"), + ] + + operations = [ + migrations.AlterModelOptions( + name="tag", + options={"ordering": ["name"]}, + ), + ] diff --git a/articles/models.py b/articles/models.py index c98d700..fa84a7c 100644 --- a/articles/models.py +++ b/articles/models.py @@ -23,6 +23,16 @@ class User(AbstractUser): pass +class Tag(models.Model): + name = models.CharField(max_length=255, unique=True) + + class Meta: + ordering = ["name"] + + def __str__(self): + return self.name + + class Article(models.Model): DRAFT = "draft" PUBLISHED = "published" @@ -39,11 +49,11 @@ class Article(models.Model): author = models.ForeignKey(User, on_delete=models.PROTECT, default=1) views_count = models.IntegerField(default=0) slug = models.SlugField(unique=True, max_length=255) - keywords = models.CharField(max_length=255, blank=True) has_code = models.BooleanField(default=False, blank=True) is_home = models.BooleanField(default=False, blank=True) custom_css = models.TextField(blank=True) draft_key = models.UUIDField(default=uuid.uuid4) + tags = models.ManyToManyField(to=Tag, related_name="articles") class Meta: ordering = ["-published_at"] @@ -105,22 +115,14 @@ class Article(models.Model): @cached_property def get_related_articles(self): related_articles = set() - for keyword in self.get_formatted_keywords: - potential_articles = Article.objects.filter( - keywords__icontains=keyword, - status=Article.PUBLISHED, - ).exclude(pk=self.pk) - for article in potential_articles: - if keyword in article.get_formatted_keywords: - related_articles.add(article) + for tag in self.tags.all(): + related_articles.update(tag.articles.all()) sample_size = min([len(related_articles), 3]) return random.sample(related_articles, sample_size) @cached_property - def get_formatted_keywords(self): - return list( - filter(None, map(lambda k: k.strip().lower(), self.keywords.split(","))) - ) + def keywords(self): + return ", ".join(map(lambda tag: tag.name, self.tags.all())) @cached_property def get_minified_custom_css(self): diff --git a/articles/views/html.py b/articles/views/html.py index 2ddb376..b9b3413 100644 --- a/articles/views/html.py +++ b/articles/views/html.py @@ -74,9 +74,9 @@ class SearchArticlesListView(BaseArticleListView): operator.and_, (Q(content__icontains=term) for term in search_terms) ) | reduce( - operator.and_, (Q(keywords__icontains=term) for term in search_terms) + operator.and_, (Q(tags__name__icontains=term) for term in search_terms) ) - ) + ).distinct() def get_additional_querystring_params(self) -> Dict[str, str]: search_expression = self.request.GET.get("s")