Compare commits
7 Commits
ee4b010411
...
98884e2ddf
| Author | SHA1 | Date |
|---|---|---|
|
|
98884e2ddf | |
|
|
f4000948b4 | |
|
|
63bf8211a6 | |
|
|
7538220e9a | |
|
|
284bddd0d8 | |
|
|
f3c6b66e7d | |
|
|
988483a9bc |
|
|
@ -45,6 +45,7 @@ INSTALLED_APPS = [
|
||||||
"django.contrib.staticfiles",
|
"django.contrib.staticfiles",
|
||||||
"core",
|
"core",
|
||||||
"dashboard",
|
"dashboard",
|
||||||
|
"news",
|
||||||
"markdownx",
|
"markdownx",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -129,7 +130,7 @@ USE_TZ = True
|
||||||
|
|
||||||
STATIC_URL = "static/"
|
STATIC_URL = "static/"
|
||||||
STATIC_ROOT = BASE_DIR / "static"
|
STATIC_ROOT = BASE_DIR / "static"
|
||||||
MEDIA_URL = "media"
|
MEDIA_URL = "media/"
|
||||||
MEDIA_ROOT = BASE_DIR / "media"
|
MEDIA_ROOT = BASE_DIR / "media"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,5 +23,6 @@ urlpatterns = [
|
||||||
path("", include("core.urls", namespace="core")),
|
path("", include("core.urls", namespace="core")),
|
||||||
path("accounts/", include("accounts.urls")),
|
path("accounts/", include("accounts.urls")),
|
||||||
path("dashboard/", include("dashboard.urls", namespace="dashboard")),
|
path("dashboard/", include("dashboard.urls", namespace="dashboard")),
|
||||||
|
path("news/", include("news.urls", namespace="news")),
|
||||||
path("markdownx/", include("markdownx.urls")),
|
path("markdownx/", include("markdownx.urls")),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,30 @@
|
||||||
|
import tagulous.admin
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from markdownx.admin import MarkdownxModelAdmin
|
||||||
|
|
||||||
# Register your models here.
|
from .models import Category, NewsItem
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Category)
|
||||||
|
class CategoryAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ["title", "slug"]
|
||||||
|
prepopulated_fields = {"slug": ("title",)}
|
||||||
|
|
||||||
|
|
||||||
|
class NewsItemAdmin(MarkdownxModelAdmin):
|
||||||
|
list_display = [
|
||||||
|
"title",
|
||||||
|
"category",
|
||||||
|
"tags",
|
||||||
|
"created_at",
|
||||||
|
"is_published",
|
||||||
|
"is_featured",
|
||||||
|
"owner",
|
||||||
|
]
|
||||||
|
list_filter = ["is_published", "is_featured", "category", "tags", "owner"]
|
||||||
|
ordering = ["is_published", "-created_at"]
|
||||||
|
prepopulated_fields = {"slug": ("title",)}
|
||||||
|
list_display_links = ["title"]
|
||||||
|
|
||||||
|
|
||||||
|
tagulous.admin.register(NewsItem, NewsItemAdmin)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
# Generated by Django 5.2.8 on 2025-11-09 21:45
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import markdownx.models
|
||||||
|
import tagulous.models.fields
|
||||||
|
import tagulous.models.models
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Category",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("title", models.CharField(max_length=40, unique=True)),
|
||||||
|
("slug", models.SlugField(max_length=40, unique=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"ordering": ["title"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Tagulous_NewsItem_tags",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=255, unique=True)),
|
||||||
|
("slug", models.SlugField()),
|
||||||
|
(
|
||||||
|
"count",
|
||||||
|
models.IntegerField(
|
||||||
|
default=0,
|
||||||
|
help_text="Internal counter of how many times this tag is in use",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"protected",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Will not be deleted when the count reaches 0",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"ordering": ("name",),
|
||||||
|
"abstract": False,
|
||||||
|
"unique_together": {("slug",)},
|
||||||
|
},
|
||||||
|
bases=(tagulous.models.models.BaseTagModel, models.Model),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="NewsItem",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("title", models.CharField(max_length=200)),
|
||||||
|
("slug", models.SlugField(max_length=200, unique=True)),
|
||||||
|
("body", markdownx.models.MarkdownxField()),
|
||||||
|
("origin_link", models.URLField(blank=True, null=True)),
|
||||||
|
("origin_times_followed", models.PositiveIntegerField()),
|
||||||
|
("header_img", models.ImageField(blank=True, null=True, upload_to="")),
|
||||||
|
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("is_published", models.BooleanField(default=False)),
|
||||||
|
("is_featured", models.BooleanField(default=False)),
|
||||||
|
(
|
||||||
|
"category",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="news_items",
|
||||||
|
to="news.category",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"owner",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="news_items",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"tags",
|
||||||
|
tagulous.models.fields.TagField(
|
||||||
|
_set_tag_meta=True,
|
||||||
|
force_lowercase=True,
|
||||||
|
help_text="Enter a comma-separated tag string",
|
||||||
|
to="news.tagulous_newsitem_tags",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"ordering": ["-created_at"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
|
|
@ -16,10 +17,51 @@ class Category(models.Model):
|
||||||
return super(Category, self).save()
|
return super(Category, self).save()
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
pass
|
return reverse_lazy("news:list_category", args=[self.slug])
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["title"]
|
ordering = ["title"]
|
||||||
|
|
||||||
|
|
||||||
|
class NewsItem(models.Model):
|
||||||
|
title = models.CharField(max_length=200)
|
||||||
|
slug = models.SlugField(max_length=200, unique=True)
|
||||||
|
category = models.ForeignKey(
|
||||||
|
Category, on_delete=models.PROTECT, related_name="news_items"
|
||||||
|
)
|
||||||
|
body = MarkdownxField()
|
||||||
|
origin_link = models.URLField(blank=True, null=True)
|
||||||
|
origin_times_followed = models.PositiveIntegerField()
|
||||||
|
header_img = models.ImageField(blank=True, null=True)
|
||||||
|
tags = TagField(
|
||||||
|
force_lowercase=True,
|
||||||
|
get_absolute_url=lambda tag: reverse_lazy(
|
||||||
|
"news:list_tag", kwargs={"tag": tag.slug}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
is_published = models.BooleanField(default=False)
|
||||||
|
is_featured = models.BooleanField(default=False)
|
||||||
|
owner = models.ForeignKey(
|
||||||
|
get_user_model(), on_delete=models.CASCADE, related_name="news_items"
|
||||||
|
)
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
if not self.slug:
|
||||||
|
self.slug = slugify(self.title)
|
||||||
|
return super(NewsItem, self).save()
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse_lazy("news:detail", args=[self.category.slug, self.slug])
|
||||||
|
|
||||||
|
def body_as_markdown(self):
|
||||||
|
return markdownify(self.body)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["-created_at"]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main>
|
||||||
|
{% if object_list %}
|
||||||
|
<h3>Category List</h3>
|
||||||
|
<hr>
|
||||||
|
<ul>
|
||||||
|
{% for category in object_list %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ category.get_absolute_url }}">{{ category.title }}</a>
|
||||||
|
<!-- {% if category.introduction %} -->
|
||||||
|
<!-- <hr> -->
|
||||||
|
<!-- <p>{{ category.introduction }}</p> -->
|
||||||
|
<!-- {% endif %} -->
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
{% for tag in newsitem.tags.all %}
|
||||||
|
<a href="{{ tag.get_absolute_url }}">{{ tag }}</a>{% if tag != newsitem.tags.last %},{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article>
|
||||||
|
<h3>{{ newsitem.title }}</h3>
|
||||||
|
<p>{{ newsitem.created_at | date:'jS \o\f F, Y' }}</p>
|
||||||
|
<hr>
|
||||||
|
<div>
|
||||||
|
{{ newsitem.body_as_markdown|safe }}
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div>
|
||||||
|
<span>Posted in the <a href="{% url 'news:list_category' category=newsitem.category.slug %}">{{ newsitem.category }}</a> category</span>
|
||||||
|
<span>{% include 'news/includes/display_tags.html' %}</span>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main>
|
||||||
|
{% if object_list %}
|
||||||
|
{% if category %}
|
||||||
|
<h3>{{ category }} News</h3>
|
||||||
|
<!-- {% if category.introduction %} -->
|
||||||
|
<!-- <div> -->
|
||||||
|
<!-- <p>{{ category.introduction }}</p> -->
|
||||||
|
<!-- </div> -->
|
||||||
|
<!-- {% endif %} -->
|
||||||
|
{% elif tag %}
|
||||||
|
<h3>News for tag: {{ tag }}</h3>
|
||||||
|
{% else %}
|
||||||
|
<h3>Recent News</h3>
|
||||||
|
{% endif %}
|
||||||
|
<hr>
|
||||||
|
<div>
|
||||||
|
{% for news in object_list %}
|
||||||
|
<div>
|
||||||
|
<a href="{{ news.get_absolute_url }}">{{ news.title }}</a>
|
||||||
|
<hr >
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
<a href="{% url 'news:list_category' category=news.category.slug %}">{{ news.category }} </a>
|
||||||
|
• {{ news.created | date:'jS \o\f F, Y' }}
|
||||||
|
</span>
|
||||||
|
<span>{% include 'news/includes/display_tags.html' %}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div>
|
||||||
|
<h3>Oops!</h3>
|
||||||
|
<h5>It looks like there is no news for that {% if category %}category{% elif tag %}tag{% endif %} yet.</h5>
|
||||||
|
<p>(sorry not sorry)</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if page_obj.has_previous or page_obj.has_next %}
|
||||||
|
{% include 'pagination.html' %}
|
||||||
|
{% endif %}
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main>
|
||||||
|
{% if object_list %}
|
||||||
|
<h3>List of Tags</h3>
|
||||||
|
<hr>
|
||||||
|
<ul>
|
||||||
|
{% for tag in object_list %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ tag.get_absolute_url }}">{{ tag }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from .views import CategoryList, NewsItemDetail, NewsItemList, TagList
|
||||||
|
|
||||||
|
app_name = "news"
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("", NewsItemList.as_view(), name="list"),
|
||||||
|
path("categories/", CategoryList.as_view(), name="categories"),
|
||||||
|
path("categories/<slug:category>/", NewsItemList.as_view(), name="list_category"),
|
||||||
|
path("tags/", TagList.as_view(), name="tags"),
|
||||||
|
path("tags/<slug:tag>/", NewsItemList.as_view(), name="list_tag"),
|
||||||
|
path("<slug:category>/<slug:slug>/", NewsItemDetail.as_view(), name="detail"),
|
||||||
|
]
|
||||||
|
|
@ -1,3 +1,44 @@
|
||||||
from django.shortcuts import render
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.views.generic import DetailView, ListView
|
||||||
|
|
||||||
# Create your views here.
|
from .models import Category, NewsItem
|
||||||
|
|
||||||
|
|
||||||
|
class CategoryList(ListView):
|
||||||
|
model = Category
|
||||||
|
|
||||||
|
|
||||||
|
class NewsItemDetail(DetailView):
|
||||||
|
model = NewsItem
|
||||||
|
|
||||||
|
|
||||||
|
class NewsItemList(ListView):
|
||||||
|
category = None
|
||||||
|
tag = None
|
||||||
|
paginate_by = 6
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
category_slug = self.kwargs.get("category", None)
|
||||||
|
self.tag = self.kwargs.get("tag", None)
|
||||||
|
if category_slug:
|
||||||
|
self.category = get_object_or_404(Category, slug=category_slug)
|
||||||
|
return NewsItem.objects.filter(category=self.category).filter(
|
||||||
|
is_published=True
|
||||||
|
)
|
||||||
|
elif self.tag:
|
||||||
|
return NewsItem.objects.filter(tags=self.tag).filter(is_published=True)
|
||||||
|
else:
|
||||||
|
return NewsItem.objects.filter(is_published=True)
|
||||||
|
|
||||||
|
def get_context_data(self, *, object_list=None, **kwargs):
|
||||||
|
context = super(NewsItemList, self).get_context_data(**kwargs)
|
||||||
|
context["category"] = self.category
|
||||||
|
context["tag"] = self.tag
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class TagList(ListView):
|
||||||
|
template_name = "news/tag_list.html"
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return NewsItem.tags.tag_model.objects.all()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue