From 812ed9ea1574abe11d17137cb30ff10684150a69 Mon Sep 17 00:00:00 2001 From: Natalia <124304+nessita@users.noreply.github.com> Date: Thu, 15 Jun 2023 18:01:59 -0300 Subject: [PATCH] Added specialized News model to differentiate general Entry from News --- config/urls.py | 6 +++-- news/admin.py | 5 ++-- news/forms.py | 8 ++++++- news/migrations/0005_news.py | 35 ++++++++++++++++++++++++++++ news/models.py | 27 +++++++++++++++++++--- news/tests/test_acl.py | 5 +--- news/tests/test_forms.py | 13 ++++++++++- news/tests/test_models.py | 14 ++++++++++- news/tests/test_views.py | 45 ++++++++++++++++++++++++++++-------- news/views.py | 31 +++++++++++++++++-------- templates/news/list.html | 10 ++++++-- 11 files changed, 164 insertions(+), 35 deletions(-) create mode 100644 news/migrations/0005_news.py diff --git a/config/urls.py b/config/urls.py index 987f7a14..37c9c68f 100755 --- a/config/urls.py +++ b/config/urls.py @@ -37,7 +37,6 @@ from news.views import ( BlogPostCreateView, BlogPostListView, EntryApproveView, - EntryCreateView, EntryDeleteView, EntryDetailView, EntryListView, @@ -45,6 +44,8 @@ from news.views import ( EntryUpdateView, LinkCreateView, LinkListView, + NewsCreateView, + NewsListView, PollCreateView, PollListView, VideoCreateView, @@ -141,9 +142,10 @@ urlpatterns = ( path("news/", EntryListView.as_view(), name="news"), path("news/blogpost/", BlogPostListView.as_view(), name="news-blogpost-list"), path("news/link/", LinkListView.as_view(), name="news-link-list"), + path("news/news/", NewsListView.as_view(), name="news-news-list"), path("news/poll/", PollListView.as_view(), name="news-poll-list"), path("news/video/", VideoListView.as_view(), name="news-video-list"), - path("news/add/", EntryCreateView.as_view(), name="news-create"), + path("news/add/", NewsCreateView.as_view(), name="news-create"), path( "news/add/blogpost/", BlogPostCreateView.as_view(), diff --git a/news/admin.py b/news/admin.py index 95d735f0..e8d22a70 100644 --- a/news/admin.py +++ b/news/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from .models import Entry +from .models import NEWS_MODELS class EntryAdmin(admin.ModelAdmin): @@ -9,4 +9,5 @@ class EntryAdmin(admin.ModelAdmin): prepopulated_fields = {"slug": ["title"]} -admin.site.register(Entry, EntryAdmin) +for news_model in NEWS_MODELS: + admin.site.register(news_model, EntryAdmin) diff --git a/news/forms.py b/news/forms.py index 32c3dd33..11776980 100644 --- a/news/forms.py +++ b/news/forms.py @@ -1,5 +1,5 @@ from django import forms -from .models import BlogPost, Entry, Link, Poll, Video +from .models import BlogPost, Entry, Link, News, Poll, Video class EntryForm(forms.ModelForm): @@ -29,6 +29,12 @@ class LinkForm(EntryForm): fields = ["title", "publish_at", "external_url", "image"] +class NewsForm(EntryForm): + class Meta: + model = News + fields = ["title", "publish_at", "content", "image"] + + class PollForm(EntryForm): class Meta: model = Poll diff --git a/news/migrations/0005_news.py b/news/migrations/0005_news.py new file mode 100644 index 00000000..9563f6eb --- /dev/null +++ b/news/migrations/0005_news.py @@ -0,0 +1,35 @@ +# Generated by Django 4.2.1 on 2023-06-15 20:44 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("news", "0004_alter_entry_image"), + ] + + operations = [ + migrations.CreateModel( + name="News", + fields=[ + ( + "entry_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="news.entry", + ), + ), + ], + options={ + "verbose_name": "News", + "verbose_name_plural": "News", + }, + bases=("news.entry",), + ), + ] diff --git a/news/models.py b/news/models.py index dd349e29..0f737fc2 100644 --- a/news/models.py +++ b/news/models.py @@ -35,6 +35,7 @@ class Entry(models.Model): class AlreadyApprovedError(Exception): """The entry cannot be approved again.""" + news_type = None slug = models.SlugField() title = models.CharField(max_length=255) content = models.TextField(blank=True, default="") @@ -92,6 +93,14 @@ class Entry(models.Model): result = False return result + @cached_property + def is_news(self): + try: + result = self.news is not None + except News.DoesNotExist: + result = False + return result + @cached_property def is_poll(self): try: @@ -142,22 +151,31 @@ class Entry(models.Model): return acl.author_needs_moderation(self) +class News(Entry): + news_type = "news" + + class Meta: + verbose_name = "News" + verbose_name_plural = "News" + + class BlogPost(Entry): + news_type = "blogpost" abstract = models.CharField(max_length=256) # Possible extra fields: RSS feed? banner? keywords? tags? class Link(Entry): - pass + news_type = "link" class Video(Entry): - pass + news_type = "video" # Possible extra fields: length? quality? class Poll(Entry): - pass + news_type = "poll" # Possible extra fields: voting expiration date? @@ -166,3 +184,6 @@ class PollChoice(models.Model): wording = models.CharField(max_length=200) order = models.PositiveIntegerField() votes = models.ManyToManyField(User) + + +NEWS_MODELS = [Entry, BlogPost, Link, News, Poll, Video] diff --git a/news/tests/test_acl.py b/news/tests/test_acl.py index 8a98f110..f68ee223 100644 --- a/news/tests/test_acl.py +++ b/news/tests/test_acl.py @@ -9,10 +9,7 @@ from ..acl import ( can_view, moderators, ) -from ..models import BlogPost, Entry, Link, Poll, Video - - -NEWS_MODELS = [Entry, BlogPost, Link, Poll, Video] +from ..models import NEWS_MODELS def test_moderators_empty(): diff --git a/news/tests/test_forms.py b/news/tests/test_forms.py index 67a15cd8..8c0f9e74 100644 --- a/news/tests/test_forms.py +++ b/news/tests/test_forms.py @@ -3,7 +3,7 @@ import datetime from django.utils.timezone import now from model_bakery import baker -from ..forms import BlogPostForm, EntryForm, LinkForm, PollForm, VideoForm +from ..forms import BlogPostForm, EntryForm, LinkForm, NewsForm, PollForm, VideoForm from ..models import Entry @@ -138,6 +138,17 @@ def test_link_form(): ] +def test_news_form(): + form = NewsForm() + assert isinstance(form, EntryForm) + assert sorted(form.fields.keys()) == [ + "content", + "image", + "publish_at", + "title", + ] + + def test_poll_form(): form = PollForm() assert isinstance(form, EntryForm) diff --git a/news/tests/test_models.py b/news/tests/test_models.py index d73b88e3..ad86d956 100644 --- a/news/tests/test_models.py +++ b/news/tests/test_models.py @@ -8,9 +8,10 @@ from model_bakery import baker from ..models import Entry, Poll -def test_entry_str(): +def test_entry_basic(): entry = baker.make("Entry") assert str(entry) == entry.title + assert entry.news_type is None def test_entry_generate_slug(): @@ -272,24 +273,35 @@ def test_entry_manager_custom_queryset(make_entry): def test_blogpost(): blogpost = baker.make("BlogPost") assert isinstance(blogpost, Entry) + assert blogpost.news_type == "blogpost" assert Entry.objects.get(id=blogpost.id).blogpost == blogpost def test_link(): link = baker.make("Link") assert isinstance(link, Entry) + assert link.news_type == "link" assert Entry.objects.get(id=link.id).link == link +def test_news(): + news = baker.make("News") + assert isinstance(news, Entry) + assert news.news_type == "news" + assert Entry.objects.get(id=news.id).news == news + + def test_video(): video = baker.make("Video") assert isinstance(video, Entry) + assert video.news_type == "video" assert Entry.objects.get(id=video.id).video == video def test_poll(): poll = baker.make("Poll") assert isinstance(poll, Entry) + assert poll.news_type == "poll" assert Entry.objects.get(id=poll.id).poll == poll diff --git a/news/tests/test_views.py b/news/tests/test_views.py index 1c5304dc..9bbaac63 100644 --- a/news/tests/test_views.py +++ b/news/tests/test_views.py @@ -9,21 +9,18 @@ from django.utils.text import slugify from django.utils.timezone import now from model_bakery import baker - -from ..forms import BlogPostForm, EntryForm, LinkForm, PollForm, VideoForm -from ..models import BlogPost, Entry, Link, Poll, Video +from ..forms import BlogPostForm, LinkForm, NewsForm, PollForm, VideoForm +from ..models import NEWS_MODELS, BlogPost, Entry, Link, News, Poll, Video from ..notifications import send_email_after_approval, send_email_news_needs_moderation -NEWS_MODELS = [Entry, BlogPost, Link, Poll, Video] - - @pytest.mark.parametrize( "url_name, model_class", [ ("news", Entry), ("news-blogpost-list", BlogPost), ("news-link-list", Link), + ("news-news-list", News), ("news-poll-list", Poll), ("news-video-list", Video), ], @@ -64,7 +61,8 @@ def test_entry_list( for n in expected: assert n.get_absolute_url() in content assert n.title in content - assert model_class.__name__.lower() in content # this is the tag + if n.news_type: + assert n.news_type in content # this is the tag assert not_approved_news.get_absolute_url() not in content assert not_approved_news.title not in content @@ -79,12 +77,41 @@ def test_entry_list( assert (tp.reverse("news-video-create") in content) == authenticated +def test_entry_list_queries(tp, make_entry): + expected = [ + make_entry(model_class) + for model_class in NEWS_MODELS + for i in range(len(model_class.__name__)) + ] + + # 4 queries + response = tp.assertGoodView(tp.reverse("news"), test_query_count=6, verbose=True) + + # assert set(response.context.get("entry_list", [])) == set(expected) + + content = str(response.content) + for n in expected: + assert n.get_absolute_url() in content + assert n.title in content + news_type_tag = ( + f'' + f"{n.news_type}" + f"" + ) + if n.news_type is None: + tp.assertResponseNotContains(news_type_tag, response) + else: + tp.assertResponseContains(news_type_tag, response) # this is the tag + + @pytest.mark.parametrize( "url_name, model_class", [ ("news", Entry), ("news-blogpost-list", BlogPost), ("news-link-list", Link), + ("news-news-list", News), ("news-poll-list", Poll), ("news-video-list", Video), ], @@ -252,7 +279,7 @@ def test_news_detail_next_url(tp, make_entry, moderator_user, model_class): @pytest.mark.parametrize( "url_name, form_class", [ - ("news-create", EntryForm), + ("news-create", NewsForm), ("news-blogpost-create", BlogPostForm), ("news-link-create", LinkForm), ("news-poll-create", PollForm), @@ -279,7 +306,7 @@ def test_news_create_get(tp, regular_user, url_name, form_class): @pytest.mark.parametrize( "url_name, model_class, data_fields", [ - ("news-create", Entry, EntryForm.Meta.fields), + ("news-create", News, NewsForm.Meta.fields), ("news-blogpost-create", BlogPost, BlogPostForm.Meta.fields), ("news-link-create", Link, LinkForm.Meta.fields), ("news-poll-create", Poll, PollForm.Meta.fields), diff --git a/news/views.py b/news/views.py index 9c9bd64d..69d28dc6 100644 --- a/news/views.py +++ b/news/views.py @@ -17,8 +17,8 @@ from django.views.generic import ( from django.views.generic.detail import SingleObjectMixin from .acl import can_approve -from .forms import BlogPostForm, EntryForm, LinkForm, PollForm, VideoForm -from .models import BlogPost, Entry, Link, Poll, Video +from .forms import BlogPostForm, EntryForm, LinkForm, NewsForm, PollForm, VideoForm +from .models import BlogPost, Entry, Link, News, Poll, Video from .notifications import send_email_after_approval, send_email_news_needs_moderation @@ -35,24 +35,22 @@ class EntryListView(ListView): model = Entry template_name = "news/list.html" ordering = ["-publish_at"] - paginate_by = 100 # XXX: use pagination in the template! Issue #377 + paginate_by = None # XXX: use pagination in the template! Issue #377 context_object_name = "entry_list" # Ensure children use the same name def get_queryset(self): result = super().get_queryset().filter(published=True) if self.model == Entry: - result = result.select_related("blogpost", "link", "poll", "video") result = result.annotate( tag=Case( When(blogpost__entry_ptr__isnull=False, then=Value("blogpost")), When(link__entry_ptr__isnull=False, then=Value("link")), + When(news__entry_ptr__isnull=False, then=Value("news")), When(poll__entry_ptr__isnull=False, then=Value("poll")), When(video__entry_ptr__isnull=False, then=Value("video")), default=Value(""), ) ) - else: - result = result # .select_related("entry_ptr") return result @@ -64,6 +62,10 @@ class LinkListView(EntryListView): model = Link +class NewsListView(EntryListView): + model = News + + class PollListView(EntryListView): model = Poll @@ -110,11 +112,11 @@ class EntryDetailView(DetailView): class EntryCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView): - model = Entry - form_class = EntryForm + model = None + form_class = None template_name = "news/form.html" - add_label = _("Create News") - add_url_name = "news-create" + add_label = None + add_url_name = None success_message = _("The news entry was successfully created.") def form_valid(self, form): @@ -145,6 +147,13 @@ class LinkCreateView(EntryCreateView): add_url_name = "news-link-create" +class NewsCreateView(EntryCreateView): + model = News + form_class = NewsForm + add_label = _("Create News") + add_url_name = "news-create" + + class PollCreateView(EntryCreateView): model = Poll form_class = PollForm @@ -200,6 +209,8 @@ class EntryUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView): result = BlogPostForm elif self.object.is_link: result = LinkForm + elif self.object.is_news: + result = NewsForm elif self.object.is_poll: result = PollForm elif self.object.is_video: diff --git a/templates/news/list.html b/templates/news/list.html index 63208c90..95ede599 100644 --- a/templates/news/list.html +++ b/templates/news/list.html @@ -58,7 +58,7 @@