mirror of
https://github.com/boostorg/website-v2.git
synced 2026-01-19 04:42:17 +00:00
Added specialized News model to differentiate general Entry from News
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
35
news/migrations/0005_news.py
Normal file
35
news/migrations/0005_news.py
Normal file
@@ -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",),
|
||||
),
|
||||
]
|
||||
@@ -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]
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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'<a data-test="news-tag" href="/news/{n.news_type}/" '
|
||||
f'class="px-3 text-sm rounded-md border-orange bg-orange">'
|
||||
f"<strong>{n.news_type}</strong>"
|
||||
f"</a>"
|
||||
)
|
||||
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),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
<div class="py-5 w-1/6">
|
||||
{% url 'news' as target_url %}
|
||||
<a href="{{ target_url }}" class="py-4 px-6 rounded-md border-orange {% if request.path == target_url %}bg-gray-300{% else %}bg-orange{% endif %}">
|
||||
{% translate "News" %}
|
||||
{% translate "All News" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="py-5 w-1/6">
|
||||
@@ -73,6 +73,12 @@
|
||||
{% translate "Links" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="py-5 w-1/6">
|
||||
{% url 'news-news-list' as target_url %}
|
||||
<a href="{{ target_url }}" class="py-4 px-6 rounded-md border-orange {% if request.path == target_url %}bg-gray-300{% else %}bg-orange{% endif %}">
|
||||
{% translate "News" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="py-5 w-1/6">
|
||||
{% url 'news-poll-list' as target_url %}
|
||||
<a href="{{ target_url }}" class="py-4 px-6 rounded-md border-orange {% if request.path == target_url %}bg-gray-300{% else %}bg-orange{% endif %}">
|
||||
@@ -99,7 +105,7 @@
|
||||
<p><a href="{{ entry.get_absolute_url }}">{{ entry.title }}</a></p>
|
||||
{% if entry.tag %}
|
||||
{% with url_name="news-"|add:entry.tag|add:"-list" %}
|
||||
<a href="{% url url_name %}" class="px-3 text-sm rounded-md border-orange bg-orange">
|
||||
<a data-test="news-tag" href="{% url url_name %}" class="px-3 text-sm rounded-md border-orange bg-orange">
|
||||
<strong>{{ entry.tag }}</strong>
|
||||
</a>
|
||||
{% endwith %}
|
||||
|
||||
Reference in New Issue
Block a user