Add tagging and trending functionality to ArticlePage
- Created ArticlePageTag model for tagging articles. - Added ClusterTaggableManager to ArticlePage for tag management. - Renamed 'recommended' field to 'trending' in ArticlePage. - Updated migrations to reflect changes in models. - Implemented methods to retrieve trending articles and related articles based on tags. - Modified templates to display tags and related articles on article pages. - Refactored category and homepage templates to accommodate new sections and layouts. - Removed deprecated templates and added new includes for better modularity. - Updated header to include links for latest and trending articles. - Added environment variable support for configurable block and page sizes. - Updated requirements to include python-dotenv for environment variable management.
This commit is contained in:
parent
7c9fe7f6f9
commit
b04ad110a6
11
innovedus_cms/home/context_processors.py
Normal file
11
innovedus_cms/home/context_processors.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from home.models import LatestPage, TrendingPage
|
||||||
|
|
||||||
|
|
||||||
|
def navigation_pages(request):
|
||||||
|
"""
|
||||||
|
Provide Latest/Trending page references for site-wide navigation.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"nav_latest_page": LatestPage.objects.live().first(),
|
||||||
|
"nav_trending_page": TrendingPage.objects.live().first(),
|
||||||
|
}
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
import django.db.models.deletion
|
||||||
|
import modelcluster.contrib.taggit
|
||||||
|
import modelcluster.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("home", "0008_alter_articlepage_banner_image_and_more"),
|
||||||
|
("taggit", "0004_alter_taggeditem_content_type_alter_taggeditem_tag"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="ArticlePageTag",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"content_object",
|
||||||
|
modelcluster.fields.ParentalKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="tagged_items",
|
||||||
|
to="home.articlepage",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"tag",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="home_articlepagetag_items",
|
||||||
|
to="taggit.tag",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="articlepage",
|
||||||
|
name="tags",
|
||||||
|
field=modelcluster.contrib.taggit.ClusterTaggableManager(
|
||||||
|
blank=True,
|
||||||
|
help_text="A comma-separated list of tags.",
|
||||||
|
through="home.ArticlePageTag",
|
||||||
|
to="taggit.Tag",
|
||||||
|
verbose_name="Tags",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("home", "0009_articlepagetag_articlepage_tags"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="articlepage",
|
||||||
|
name="recommended",
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="在熱門區塊顯示",
|
||||||
|
verbose_name="Trending",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("home", "0010_alter_articlepage_recommended"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameModel(
|
||||||
|
old_name="RecommendedPage",
|
||||||
|
new_name="TrendingPage",
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-11-06 04:04
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('home', '0011_rename_recommendedpage_trendingpage'),
|
||||||
|
('taggit', '0006_rename_taggeditem_content_type_object_id_taggit_tagg_content_8fc721_idx'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='articlepage',
|
||||||
|
old_name='recommended',
|
||||||
|
new_name='trending',
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='articlepagetag',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='articlepagetag',
|
||||||
|
name='tag',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_items', to='taggit.tag'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -1,28 +1,47 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
from wagtail.models import Page
|
from wagtail.models import Page
|
||||||
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
||||||
|
from modelcluster.contrib.taggit import ClusterTaggableManager
|
||||||
|
from modelcluster.fields import ParentalKey
|
||||||
|
from taggit.models import TaggedItemBase
|
||||||
|
|
||||||
BLOCK_SIZE = 5
|
def _get_env_int(name, default):
|
||||||
PAGE_SIZE = 10
|
value = os.environ.get(name)
|
||||||
|
if value is None:
|
||||||
|
return default
|
||||||
|
try:
|
||||||
|
return int(value)
|
||||||
|
except ValueError:
|
||||||
|
return default
|
||||||
|
|
||||||
|
BLOCK_SIZE = _get_env_int("HOMEPAGE_BLOCK_SIZE", 5) # Default to 5 articles in block layout
|
||||||
|
HORIZON_SIZE = _get_env_int("HOMEPAGE_HORIZON_SIZE", 8) # Default to 8 articles in horizon layout
|
||||||
|
PAGE_SIZE = _get_env_int("HOMEPAGE_PAGE_SIZE", 10) # Default to 10 articles per page for pagination
|
||||||
|
|
||||||
|
# Mixin for Category-related functionality
|
||||||
class CategoryMixin:
|
class CategoryMixin:
|
||||||
|
# Build category blocks
|
||||||
def build_category_blocks(self, request=None):
|
def build_category_blocks(self, request=None):
|
||||||
blocks = []
|
blocks = []
|
||||||
subcategories = self.get_children().type(CategoryPage).live()
|
subcategories = self.get_children().type(CategoryPage).live()
|
||||||
if subcategories.exists():
|
if subcategories.exists():
|
||||||
|
# If there are subcategories, create blocks for each
|
||||||
for category in subcategories:
|
for category in subcategories:
|
||||||
blocks.append(
|
blocks.append(
|
||||||
{
|
{
|
||||||
"title": category.title,
|
"title": category.title,
|
||||||
"items": ArticlePage.objects.child_of(category)
|
"items": ArticlePage.objects.child_of(category)
|
||||||
.live()
|
.live()
|
||||||
.order_by("-first_published_at")[:BLOCK_SIZE],
|
.order_by("-first_published_at")[:HORIZON_SIZE],
|
||||||
"url": category.url,
|
"url": category.url,
|
||||||
|
"layout": "horizon",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
# If no subcategories, paginate articles under this category
|
||||||
paginator = Paginator(
|
paginator = Paginator(
|
||||||
ArticlePage.objects.child_of(self)
|
ArticlePage.objects.child_of(self)
|
||||||
.live()
|
.live()
|
||||||
@ -47,17 +66,31 @@ class CategoryMixin:
|
|||||||
)
|
)
|
||||||
return blocks
|
return blocks
|
||||||
|
|
||||||
|
# Build breadcrumbs
|
||||||
|
def build_breadcrumbs(self):
|
||||||
|
site = self.get_site()
|
||||||
|
site_root = site.root_page if site else None
|
||||||
|
if site_root:
|
||||||
|
ancestors = self.get_ancestors().specific().filter(depth__gt=site_root.depth)
|
||||||
|
else:
|
||||||
|
ancestors = self.get_ancestors().specific()
|
||||||
|
breadcrumbs = list(ancestors) + [self]
|
||||||
|
return breadcrumbs, site_root
|
||||||
|
|
||||||
|
# Get latest articles
|
||||||
def get_latest_articles(self, request=None):
|
def get_latest_articles(self, request=None):
|
||||||
latest_page = LatestPage.objects.first()
|
latest_page = LatestPage.objects.first()
|
||||||
if not request:
|
if not request:
|
||||||
|
# No request means no pagination (e.g., homepage)
|
||||||
return {
|
return {
|
||||||
"title": latest_page.title if latest_page else "最新文章",
|
"title": latest_page.title,
|
||||||
"items": ArticlePage.objects.live().order_by("-first_published_at")[
|
"items": ArticlePage.objects.live().order_by("-first_published_at")[
|
||||||
:BLOCK_SIZE
|
:BLOCK_SIZE
|
||||||
],
|
],
|
||||||
"url": latest_page.url if latest_page else "#",
|
"url": latest_page.url,
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
|
# Paginated view
|
||||||
paginator = Paginator(
|
paginator = Paginator(
|
||||||
ArticlePage.objects.live().order_by("-first_published_at"), PAGE_SIZE
|
ArticlePage.objects.live().order_by("-first_published_at"), PAGE_SIZE
|
||||||
)
|
)
|
||||||
@ -75,16 +108,26 @@ class CategoryMixin:
|
|||||||
"url": self.url,
|
"url": self.url,
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_recommended_articles(self, request=None):
|
def get_trending_articles(self, request=None, exclude_ids=None):
|
||||||
recommended_page = RecommendedPage.objects.first()
|
trending_page = TrendingPage.objects.first()
|
||||||
|
articles_qs = ArticlePage.objects.filter(trending=True).live().order_by(
|
||||||
|
"-first_published_at"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Exclude specified article IDs
|
||||||
|
if exclude_ids:
|
||||||
|
articles_qs = articles_qs.exclude(id__in=exclude_ids)
|
||||||
|
|
||||||
if not request:
|
if not request:
|
||||||
|
# No request means no pagination (e.g., homepage)
|
||||||
return {
|
return {
|
||||||
"title": recommended_page.title if recommended_page else "推薦文章",
|
"title": trending_page.title,
|
||||||
"items": ArticlePage.objects.filter(recommended=True).live()[:BLOCK_SIZE],
|
"items": articles_qs[:HORIZON_SIZE],
|
||||||
"url": recommended_page.url if recommended_page else "#",
|
"url": trending_page.url,
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
paginator = Paginator(ArticlePage.objects.filter(recommended=True).live(), PAGE_SIZE)
|
# Paginated view
|
||||||
|
paginator = Paginator(articles_qs, PAGE_SIZE)
|
||||||
page_number = request.GET.get("page")
|
page_number = request.GET.get("page")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -104,27 +147,43 @@ class HomePage(Page, CategoryMixin):
|
|||||||
def get_context(self, request):
|
def get_context(self, request):
|
||||||
context = super().get_context(request)
|
context = super().get_context(request)
|
||||||
|
|
||||||
category_blocks = [self.get_latest_articles(), self.get_recommended_articles()]
|
sections = {
|
||||||
|
"top_section": [],
|
||||||
|
"category_sections": [],
|
||||||
|
}
|
||||||
|
|
||||||
# 找出第一層 CategoryPage(HomePage 直屬子頁)
|
latest_section = self.get_latest_articles().copy()
|
||||||
|
latest_section["layout"] = "block"
|
||||||
|
sections["top_section"].append(latest_section)
|
||||||
|
|
||||||
|
# Exclude latest articles from trending section
|
||||||
|
latest_items = latest_section.get("items", [])
|
||||||
|
if hasattr(latest_items, "values_list"):
|
||||||
|
latest_ids = list(latest_items.values_list("id", flat=True))
|
||||||
|
else:
|
||||||
|
latest_ids = [item.id for item in latest_items]
|
||||||
|
|
||||||
|
trending_section = self.get_trending_articles(
|
||||||
|
exclude_ids=latest_ids
|
||||||
|
).copy()
|
||||||
|
trending_section["layout"] = "horizon"
|
||||||
|
sections["top_section"].append(trending_section)
|
||||||
|
|
||||||
|
# Build category sections
|
||||||
categories = CategoryPage.objects.child_of(self).live().in_menu()
|
categories = CategoryPage.objects.child_of(self).live().in_menu()
|
||||||
|
|
||||||
# 若第一層沒有,抓 descendant CategoryPage
|
|
||||||
if not categories.exists():
|
|
||||||
categories = CategoryPage.objects.descendant_of(self).live().in_menu()
|
|
||||||
|
|
||||||
for category in categories:
|
for category in categories:
|
||||||
subcategories = category.get_children().type(CategoryPage).live()
|
sections["category_sections"].append(
|
||||||
category_blocks.append(
|
|
||||||
{
|
{
|
||||||
"title": category.title,
|
"title": category.title,
|
||||||
"type": "category",
|
|
||||||
"items": ArticlePage.objects.child_of(category).live()[:BLOCK_SIZE],
|
|
||||||
"url": category.url,
|
"url": category.url,
|
||||||
|
"items": ArticlePage.objects.descendant_of(category)
|
||||||
|
.live()
|
||||||
|
.order_by("-first_published_at")[:HORIZON_SIZE],
|
||||||
|
"layout": "horizon",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
context["category_blocks"] = category_blocks
|
context["sections"] = sections
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@ -133,16 +192,22 @@ class LatestPage(Page, CategoryMixin):
|
|||||||
|
|
||||||
def get_context(self, request):
|
def get_context(self, request):
|
||||||
context = super().get_context(request)
|
context = super().get_context(request)
|
||||||
context["category_blocks"] = [self.get_latest_articles(request)]
|
context["category_sections"] = [self.get_latest_articles(request)]
|
||||||
|
breadcrumbs, site_root = self.build_breadcrumbs()
|
||||||
|
context["breadcrumbs"] = breadcrumbs
|
||||||
|
context["breadcrumb_root"] = site_root
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class RecommendedPage(Page, CategoryMixin):
|
class TrendingPage(Page, CategoryMixin):
|
||||||
template = "home/category_page.html"
|
template = "home/category_page.html"
|
||||||
|
|
||||||
def get_context(self, request):
|
def get_context(self, request):
|
||||||
context = super().get_context(request)
|
context = super().get_context(request)
|
||||||
context["category_blocks"] = [self.get_recommended_articles(request)]
|
context["category_sections"] = [self.get_trending_articles(request)]
|
||||||
|
breadcrumbs, site_root = self.build_breadcrumbs()
|
||||||
|
context["breadcrumbs"] = breadcrumbs
|
||||||
|
context["breadcrumb_root"] = site_root
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@ -153,7 +218,10 @@ class CategoryPage(Page, CategoryMixin):
|
|||||||
|
|
||||||
def get_context(self, request):
|
def get_context(self, request):
|
||||||
context = super().get_context(request)
|
context = super().get_context(request)
|
||||||
context["category_blocks"] = self.build_category_blocks(request)
|
context["category_sections"] = self.build_category_blocks(request)
|
||||||
|
breadcrumbs, site_root = self.build_breadcrumbs()
|
||||||
|
context["breadcrumbs"] = breadcrumbs
|
||||||
|
context["breadcrumb_root"] = site_root
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@ -163,6 +231,13 @@ from wagtail.images.blocks import ImageChooserBlock
|
|||||||
from wagtail.fields import StreamField
|
from wagtail.fields import StreamField
|
||||||
from .blocks import ValidatingEmbedBlock, H2HeadingBlock, HorizontalRuleBlock
|
from .blocks import ValidatingEmbedBlock, H2HeadingBlock, HorizontalRuleBlock
|
||||||
|
|
||||||
|
# HashTag for Article
|
||||||
|
class ArticlePageTag(TaggedItemBase):
|
||||||
|
content_object = ParentalKey(
|
||||||
|
"home.ArticlePage",
|
||||||
|
related_name="tagged_items",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
|
|
||||||
class ArticlePage(Page):
|
class ArticlePage(Page):
|
||||||
cover_image = models.ForeignKey(
|
cover_image = models.ForeignKey(
|
||||||
@ -194,14 +269,33 @@ class ArticlePage(Page):
|
|||||||
],
|
],
|
||||||
use_json_field=True,
|
use_json_field=True,
|
||||||
)
|
)
|
||||||
recommended = models.BooleanField(default=False, help_text="在推薦區塊顯示")
|
trending = models.BooleanField("Trending", default=False, help_text="在熱門區塊顯示")
|
||||||
|
tags = ClusterTaggableManager(through="home.ArticlePageTag", blank=True)
|
||||||
|
|
||||||
content_panels = Page.content_panels + [
|
content_panels = Page.content_panels + [
|
||||||
FieldPanel("recommended"),
|
FieldPanel("trending"),
|
||||||
FieldPanel("cover_image"),
|
FieldPanel("cover_image"),
|
||||||
FieldPanel("banner_image"),
|
FieldPanel("banner_image"),
|
||||||
FieldPanel("date"),
|
FieldPanel("date"),
|
||||||
FieldPanel("intro"),
|
FieldPanel("intro"),
|
||||||
FieldPanel("body"),
|
FieldPanel("body"),
|
||||||
|
FieldPanel("tags"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def get_context(self, request):
|
||||||
|
context = super().get_context(request)
|
||||||
|
|
||||||
|
tag_ids = list(self.tags.values_list("id", flat=True))
|
||||||
|
if tag_ids:
|
||||||
|
related_articles = (
|
||||||
|
ArticlePage.objects.live()
|
||||||
|
.exclude(id=self.id)
|
||||||
|
.filter(tags__id__in=tag_ids)
|
||||||
|
.distinct()
|
||||||
|
.order_by("-first_published_at")[:4]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
related_articles = ArticlePage.objects.none()
|
||||||
|
|
||||||
|
context["related_articles"] = related_articles
|
||||||
|
return context
|
||||||
|
|||||||
@ -13,5 +13,23 @@
|
|||||||
<div class="body">
|
<div class="body">
|
||||||
{{ page.body }}
|
{{ page.body }}
|
||||||
</div>
|
</div>
|
||||||
|
{% with tags=page.tags.all %}
|
||||||
|
{% if tags %}
|
||||||
|
<div class="tags">
|
||||||
|
<span>Hashtags:</span>
|
||||||
|
<ul>
|
||||||
|
{% for tag in tags %}
|
||||||
|
<li>#{{ tag }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% if related_articles %}
|
||||||
|
<section class="related-articles">
|
||||||
|
<h2>相關文章</h2>
|
||||||
|
{% include "home/includes/article_list.html" with items=related_articles %}
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
</article>
|
</article>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -1,9 +1,31 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load wagtailcore_tags %}
|
{% load wagtailcore_tags %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if page.has_subcategories %}
|
{% if breadcrumbs %}
|
||||||
{% include "home/includes/category_block_list.html" %}
|
<nav class="breadcrumbs" aria-label="breadcrumb">
|
||||||
|
<ol>
|
||||||
|
{% if breadcrumb_root %}
|
||||||
|
<li><a href="{{ breadcrumb_root.url }}">首頁</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% for crumb in breadcrumbs %}
|
||||||
|
{% if not breadcrumb_root or crumb.id != breadcrumb_root.id %}
|
||||||
|
<li>
|
||||||
|
{% if crumb.id == page.id %}
|
||||||
|
<span>{{ crumb.title }}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% include "home/includes/category_full_list.html" %}
|
<a href="{{ crumb.url }}">{{ crumb.title }}</a>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
{% if page.has_subcategories %}
|
||||||
|
{% for section in category_sections %}
|
||||||
|
{% include "home/includes/category_session.html" with section=section %}
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
{% include "home/includes/page-article-list.html" with category=category_sections.0 %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -1,14 +1,20 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load static %}
|
|
||||||
|
|
||||||
{% block body_class %}template-homepage{% endblock %}
|
{% block body_class %}template-homepage{% endblock %}
|
||||||
|
|
||||||
{% block extra_css %}
|
|
||||||
|
|
||||||
{% endblock extra_css %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
{% with top_section=sections.top_section %}
|
||||||
|
<h2>
|
||||||
|
<a href="{{ top_section.0.url }}">最新文章</a>
|
||||||
|
</h2>
|
||||||
|
{% for section in top_section %}
|
||||||
|
{% if section.layout == "block" %}
|
||||||
|
{% include "home/includes/block_list.html" with items=section.items %}
|
||||||
|
{% elif section.layout == "horizon" %}
|
||||||
|
{% include "home/includes/horizontal_list.html" with items=section.items %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
{% include "home/includes/category_block_list.html" with category_blocks=category_blocks %}
|
{% for section in sections.category_sections %}
|
||||||
|
{% include "home/includes/category_session.html" with section=section %}
|
||||||
|
{% endfor %}
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
21
innovedus_cms/home/templates/home/includes/article_list.html
Normal file
21
innovedus_cms/home/templates/home/includes/article_list.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{% load wagtailimages_tags static %}
|
||||||
|
|
||||||
|
<ul class="article-list">
|
||||||
|
{% for article in items %}
|
||||||
|
<li>
|
||||||
|
<article>
|
||||||
|
<a href="{{ article.url }}">
|
||||||
|
{% if article.cover_image %}
|
||||||
|
{% image article.cover_image max-200x200 as cover %}
|
||||||
|
<img src="{{ cover.url }}" alt="{{ article.title }}" height="200" width="200" style="width:200px;height:200px;object-fit:contain;display:block;"/>
|
||||||
|
{% else %}
|
||||||
|
<img src="{% static 'img/default_cover.jpg' %}" alt="{{ article.title }}" height="200" width="200" style="width:200px;height:200px;object-fit:contain;display:block;"/>
|
||||||
|
{% endif %}
|
||||||
|
{{ article.title }}
|
||||||
|
</a>
|
||||||
|
</article>
|
||||||
|
</li>
|
||||||
|
{% empty %}
|
||||||
|
<li class="empty">目前沒有文章</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
19
innovedus_cms/home/templates/home/includes/block_list.html
Normal file
19
innovedus_cms/home/templates/home/includes/block_list.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{% load wagtailimages_tags static %}
|
||||||
|
|
||||||
|
<ul class="block-list">
|
||||||
|
{% for article in items %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ article.url }}">
|
||||||
|
{% if article.cover_image %}
|
||||||
|
{% image article.cover_image max-200x200 as cover %}
|
||||||
|
<img src="{{ cover.url }}" alt="{{ article.title }}" height="200" width="200" style="width:200px;height:200px;object-fit:contain;display:block;"/>
|
||||||
|
{% else %}
|
||||||
|
<img src="{% static 'img/default_cover.jpg' %}" alt="{{ article.title }}" height="200" width="200" style="width:200px;height:200px;object-fit:contain;display:block;"/>
|
||||||
|
{% endif %}
|
||||||
|
{{ article.title }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% empty %}
|
||||||
|
<li class="empty">目前沒有文章</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
@ -1,25 +0,0 @@
|
|||||||
{% load wagtailimages_tags static %}
|
|
||||||
|
|
||||||
|
|
||||||
<div class="category-block-list">
|
|
||||||
{% for category in category_blocks %}
|
|
||||||
<section class="category-section">
|
|
||||||
<h2><a href="{{ category.url }}">{{ category.title }}</a></h2>
|
|
||||||
<ul>
|
|
||||||
{% for article in category.items %}
|
|
||||||
<li>
|
|
||||||
<a href="{{ article.url }}">
|
|
||||||
{% if article.cover_image %}
|
|
||||||
{% image article.cover_image max-200x200 as cover %}
|
|
||||||
<img src="{{ cover.url }}" alt="{{ article.title }}" height="200" width="200" style="width:200px;height:200px;object-fit:contain;display:block;"/>
|
|
||||||
{% else %}
|
|
||||||
<img src="{% static 'img/default_cover.jpg' %}" alt="{{ article.title }}" height="200" width="200" style="width:200px;height:200px;object-fit:contain;display:block;"/>
|
|
||||||
{% endif %}
|
|
||||||
{{ article.title }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
{% load wagtailimages_tags static %}
|
|
||||||
|
|
||||||
<div class="category-full-list">
|
|
||||||
{% with category=category_blocks.0 %}
|
|
||||||
<h2><a href="{{ category.url }}">{{ category.title }}</a></h2>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
{% for article in category.items %}
|
|
||||||
<article>
|
|
||||||
<li>
|
|
||||||
<a href="{{ article.url }}">
|
|
||||||
{% if article.cover_image %}
|
|
||||||
{% image article.cover_image max-200x200 as cover %}
|
|
||||||
<img src="{{ cover.url }}" alt="{{ article.title }}" height="200" width="200" style="width:200px;height:200px;object-fit:contain;display:block;"/>
|
|
||||||
{% else %}
|
|
||||||
<img src="{% static 'img/default_cover.jpg' %}" alt="{{ article.title }}" height="200" width="200" style="width:200px;height:200px;object-fit:contain;display:block;"/>
|
|
||||||
{% endif %}
|
|
||||||
{{ article.title }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<!-- <p>{{ article.search_description }}</p> -->
|
|
||||||
</article>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
{% if category.items.paginator.num_pages > 1 %}
|
|
||||||
<div class="pagination">
|
|
||||||
{% if category.items.has_previous %}
|
|
||||||
<a href="?page={{ category.items.previous_page_number }}">上一頁</a>
|
|
||||||
{% endif %}
|
|
||||||
<span>第 {{ category.items.number }} / {{ category.items.paginator.num_pages }} 頁</span>
|
|
||||||
{% if category.items.has_next %}
|
|
||||||
<a href="?page={{ category.items.next_page_number }}">下一頁</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endwith %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
{% load wagtailimages_tags static %}
|
||||||
|
|
||||||
|
<section class="article-section article-section--{{ section.layout }}">
|
||||||
|
<h2>
|
||||||
|
{% if section.url %}
|
||||||
|
<a href="{{ section.url }}">{{ section.title }}</a>
|
||||||
|
{% else %}
|
||||||
|
{{ section.title }}
|
||||||
|
{% endif %}
|
||||||
|
</h2>
|
||||||
|
{% if section.layout == "block" %}
|
||||||
|
{% include "home/includes/block_list.html" with items=section.items %}
|
||||||
|
{% elif section.layout == "horizon" %}
|
||||||
|
{% include "home/includes/horizontal_list.html" with items=section.items %}
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
{% load wagtailimages_tags static %}
|
||||||
|
|
||||||
|
<ul class="horizontal-list">
|
||||||
|
{% for article in items %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ article.url }}">
|
||||||
|
{% if article.cover_image %}
|
||||||
|
{% image article.cover_image max-200x200 as cover %}
|
||||||
|
<img src="{{ cover.url }}" alt="{{ article.title }}" height="200" width="200" style="width:200px;height:200px;object-fit:cover;display:block;"/>
|
||||||
|
{% else %}
|
||||||
|
<img src="{% static 'img/default_cover.jpg' %}" alt="{{ article.title }}" height="200" width="200" style="width:200px;height:200px;object-fit:cover;display:block;"/>
|
||||||
|
{% endif %}
|
||||||
|
<span>{{ article.title }}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% empty %}
|
||||||
|
<li class="empty">目前沒有文章</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
{% load wagtailimages_tags static %}
|
||||||
|
|
||||||
|
<div class="page-article-list">
|
||||||
|
{% with category=category_sections.0 %}
|
||||||
|
<h2><a href="{{ category.url }}">{{ category.title }}</a></h2>
|
||||||
|
|
||||||
|
{% include "home/includes/article_list.html" with items=category.items %}
|
||||||
|
|
||||||
|
{% if category.items.paginator.num_pages > 1 %}
|
||||||
|
<div class="pagination">
|
||||||
|
{% if category.items.has_previous %}
|
||||||
|
<a href="?page={{ category.items.previous_page_number }}">上一頁</a>
|
||||||
|
{% endif %}
|
||||||
|
<span>第 {{ category.items.number }} / {{ category.items.paginator.num_pages }} 頁</span>
|
||||||
|
{% if category.items.has_next %}
|
||||||
|
<a href="?page={{ category.items.next_page_number }}">下一頁</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
@ -1,52 +0,0 @@
|
|||||||
{% load i18n wagtailcore_tags %}
|
|
||||||
|
|
||||||
<header class="header">
|
|
||||||
<div class="logo">
|
|
||||||
<a href="https://wagtail.org/">
|
|
||||||
<svg class="figure-logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 342.5 126.2"><title>{% trans "Visit the Wagtail website" %}</title><path fill="#FFF" d="M84 1.9v5.7s-10.2-3.8-16.8 3.1c-4.8 5-5.2 10.6-3 18.1 21.6 0 25 12.1 25 12.1L87 27l6.8-8.3c0-9.8-8.1-16.3-9.8-16.8z"/><circle cx="85.9" cy="15.9" r="2.6"/><path d="M89.2 40.9s-3.3-16.6-24.9-12.1c-2.2-7.5-1.8-13 3-18.1C73.8 3.8 84 7.6 84 7.6V1.9C80.4.3 77 0 73.2 0 59.3 0 51.6 10.4 48.3 17.4L9.2 89.3l11-2.1-20.2 39 14.1-2.5L24.9 93c30.6 0 69.8-11 64.3-52.1z"/><path d="M102.4 27l-8.6-8.3L87 27z"/><path fill="#FFF" d="M30 84.1s1-.2 2.8-.6c1.8-.4 4.3-1 7.3-1.8 1.5-.4 3.1-.9 4.8-1.5 1.7-.6 3.5-1.2 5.2-2 1.8-.7 3.6-1.6 5.4-2.6 1.8-1 3.5-2.1 5.1-3.4.4-.3.8-.6 1.2-1l1.2-1c.7-.7 1.5-1.4 2.2-2.2.7-.7 1.3-1.5 1.9-2.3l.9-1.2.4-.6.4-.6c.2-.4.5-.8.7-1.2.2-.4.4-.8.7-1.2l.3-.6.3-.6c.2-.4.4-.8.5-1.2l.9-2.4c.2-.8.5-1.6.7-2.3.2-.7.3-1.5.5-2.1.1-.7.2-1.3.3-2 .1-.6.2-1.2.2-1.7.1-.5.1-1 .2-1.5.1-1.8.1-2.8.1-2.8l1.6.1s-.1 1.1-.2 2.9c-.1.5-.1 1-.2 1.5-.1.6-.1 1.2-.3 1.8-.1.6-.3 1.3-.4 2-.2.7-.4 1.4-.6 2.2-.2.8-.5 1.5-.8 2.4-.3.8-.6 1.6-1 2.5l-.6 1.2-.3.6-.3.6c-.2.4-.5.8-.7 1.3-.3.4-.5.8-.8 1.2-.1.2-.3.4-.4.6l-.4.6-.9 1.2c-.7.8-1.3 1.6-2.1 2.3-.7.8-1.5 1.4-2.3 2.2l-1.2 1c-.4.3-.8.6-1.3.9-1.7 1.2-3.5 2.3-5.3 3.3-1.8.9-3.7 1.8-5.5 2.5-1.8.7-3.6 1.3-5.3 1.8-1.7.5-3.3 1-4.9 1.3-3 .7-5.6 1.3-7.4 1.6-1.6.6-2.6.8-2.6.8z"/><g fill="#231F20"><path d="M127 83.9h-8.8l-12.6-36.4h7.9l9 27.5 9-27.5h7.9l9 27.5 9-27.5h7.9L153 83.9h-8.8L135.6 59 127 83.9zM200.1 83.9h-7V79c-3 3.6-7 5.4-12.1 5.4-3.8 0-6.9-1.1-9.4-3.2s-3.7-5-3.7-8.6c0-3.6 1.3-6.3 4-8 2.6-1.8 6.2-2.7 10.7-2.7h9.9v-1.4c0-4.8-2.7-7.3-8.1-7.3-3.4 0-6.9 1.2-10.5 3.7l-3.4-4.8c4.4-3.5 9.4-5.3 15.1-5.3 4.3 0 7.8 1.1 10.5 3.2 2.7 2.2 4.1 5.6 4.1 10.2v23.7zm-7.7-13.6v-3.1h-8.6c-5.5 0-8.3 1.7-8.3 5.2 0 1.8.7 3.1 2.1 4.1 1.4.9 3.3 1.4 5.7 1.4 2.4 0 4.6-.7 6.4-2.1 1.8-1.3 2.7-3.1 2.7-5.5zM241.7 47.5v31.7c0 6.4-1.7 11.3-5.2 14.5-3.5 3.2-8 4.8-13.4 4.8-5.5 0-10.4-1.7-14.8-5.1l3.6-5.8c3.6 2.7 7.1 4 10.8 4 3.6 0 6.5-.9 8.6-2.8 2.1-1.9 3.2-4.9 3.2-9v-4.7c-1.1 2.1-2.8 3.9-4.9 5.1-2.1 1.3-4.5 1.9-7.1 1.9-4.8 0-8.8-1.7-11.9-5.1-3.1-3.4-4.7-7.6-4.7-12.6s1.6-9.2 4.7-12.6c3.1-3.4 7.1-5.1 11.9-5.1 4.8 0 8.7 2 11.7 6v-5.4h7.5zm-28.4 16.8c0 3 .9 5.6 2.8 7.7 1.8 2.2 4.3 3.2 7.5 3.2 3.1 0 5.7-1 7.6-3.1 1.9-2.1 2.9-4.7 2.9-7.8 0-3.1-1-5.8-2.9-7.9-2-2.2-4.5-3.2-7.6-3.2-3.1 0-5.6 1.1-7.4 3.4-2 2.1-2.9 4.7-2.9 7.7zM260.9 53.6v18.5c0 1.7.5 3.1 1.4 4.1.9 1 2.2 1.5 3.8 1.5 1.6 0 3.2-.8 4.7-2.4l3.1 5.4c-2.7 2.4-5.7 3.6-8.9 3.6-3.3 0-6-1.1-8.3-3.4-2.3-2.3-3.5-5.3-3.5-9.1V53.6h-4.6v-6.2h4.6V36.1h7.7v11.4h9.6v6.2h-9.6zM309.5 83.9h-7V79c-3 3.6-7 5.4-12.1 5.4-3.8 0-6.9-1.1-9.4-3.2s-3.7-5-3.7-8.6c0-3.6 1.3-6.3 4-8 2.6-1.8 6.2-2.7 10.7-2.7h9.9v-1.4c0-4.8-2.7-7.3-8.1-7.3-3.4 0-6.9 1.2-10.5 3.7l-3.4-4.8c4.4-3.5 9.4-5.3 15.1-5.3 4.3 0 7.8 1.1 10.5 3.2 2.7 2.2 4.1 5.6 4.1 10.2v23.7zm-7.7-13.6v-3.1h-8.6c-5.5 0-8.3 1.7-8.3 5.2 0 1.8.7 3.1 2.1 4.1 1.4.9 3.3 1.4 5.7 1.4 2.4 0 4.6-.7 6.4-2.1 1.8-1.3 2.7-3.1 2.7-5.5zM319.3 40.2c-1-1-1.4-2.1-1.4-3.4 0-1.3.5-2.5 1.4-3.4 1-1 2.1-1.4 3.4-1.4 1.3 0 2.5.5 3.4 1.4 1 1 1.4 2.1 1.4 3.4 0 1.3-.5 2.5-1.4 3.4s-2.1 1.4-3.4 1.4c-1.3.1-2.4-.4-3.4-1.4zm7.2 43.7h-7.7V47.5h7.7v36.4zM342.5 83.9h-7.7V33.1h7.7v50.8z"/></g></svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="header-link">
|
|
||||||
{% comment %}
|
|
||||||
This works for all cases but prerelease versions:
|
|
||||||
{% endcomment %}
|
|
||||||
<a href="{% wagtail_documentation_path %}/releases/{% wagtail_release_notes_path %}">
|
|
||||||
{% trans "View the release notes" %}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main class="main">
|
|
||||||
<div class="figure">
|
|
||||||
<svg class="figure-space" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300" aria-hidden="true">
|
|
||||||
<path class="egg" fill="currentColor" d="M150 250c-42.741 0-75-32.693-75-90s42.913-110 75-110c32.088 0 75 52.693 75 110s-32.258 90-75 90z"/>
|
|
||||||
<ellipse fill="#ddd" cx="150" cy="270" rx="40" ry="7"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="main-text">
|
|
||||||
<h1>{% trans "Welcome to your new Wagtail site!" %}</h1>
|
|
||||||
<p>{% trans 'Please feel free to <a href="https://github.com/wagtail/wagtail/wiki/Slack">join our community on Slack</a>, or get started with one of the links below.' %}</p>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<footer class="footer" role="contentinfo">
|
|
||||||
<a class="option option-one" href="{% wagtail_documentation_path %}/">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M9 21c0 .5.4 1 1 1h4c.6 0 1-.5 1-1v-1H9v1zm3-19C8.1 2 5 5.1 5 9c0 2.4 1.2 4.5 3 5.7V17c0 .5.4 1 1 1h6c.6 0 1-.5 1-1v-2.3c1.8-1.3 3-3.4 3-5.7 0-3.9-3.1-7-7-7zm2.9 11.1l-.9.6V16h-4v-2.3l-.9-.6C7.8 12.2 7 10.6 7 9c0-2.8 2.2-5 5-5s5 2.2 5 5c0 1.6-.8 3.2-2.1 4.1z"/></svg>
|
|
||||||
<div>
|
|
||||||
<h2>{% trans "Wagtail Documentation" %}</h2>
|
|
||||||
<p>{% trans "Topics, references, & how-tos" %}</p>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a class="option option-two" href="{% wagtail_documentation_path %}/getting_started/tutorial.html">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/></svg>
|
|
||||||
<div>
|
|
||||||
<h2>{% trans "Tutorial" %}</h2>
|
|
||||||
<p>{% trans "Build your first Wagtail site" %}</p>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a class="option option-three" href="{% url 'wagtailadmin_home' %}">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M0 0h24v24H0z" fill="none"/><path d="M16.5 13c-1.2 0-3.07.34-4.5 1-1.43-.67-3.3-1-4.5-1C5.33 13 1 14.08 1 16.25V19h22v-2.75c0-2.17-4.33-3.25-6.5-3.25zm-4 4.5h-10v-1.25c0-.54 2.56-1.75 5-1.75s5 1.21 5 1.75v1.25zm9 0H14v-1.25c0-.46-.2-.86-.52-1.22.88-.3 1.96-.53 3.02-.53 2.44 0 5 1.21 5 1.75v1.25zM7.5 12c1.93 0 3.5-1.57 3.5-3.5S9.43 5 7.5 5 4 6.57 4 8.5 5.57 12 7.5 12zm0-5.5c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2zm9 5.5c1.93 0 3.5-1.57 3.5-3.5S18.43 5 16.5 5 13 6.57 13 8.5s1.57 3.5 3.5 3.5zm0-5.5c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2z"/></svg>
|
|
||||||
<div>
|
|
||||||
<h2>{% trans "Admin Interface" %}</h2>
|
|
||||||
<p>{% trans "Create your superuser first!" %}</p>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</footer>
|
|
||||||
1
innovedus_cms/home/templatetags/__init__.py
Normal file
1
innovedus_cms/home/templatetags/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -77,6 +77,7 @@ TEMPLATES = [
|
|||||||
"django.contrib.auth.context_processors.auth",
|
"django.contrib.auth.context_processors.auth",
|
||||||
"django.contrib.messages.context_processors.messages",
|
"django.contrib.messages.context_processors.messages",
|
||||||
"wagtail.contrib.settings.context_processors.settings",
|
"wagtail.contrib.settings.context_processors.settings",
|
||||||
|
"home.context_processors.navigation_pages",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -16,6 +16,21 @@
|
|||||||
<ul>
|
<ul>
|
||||||
{% with site_root=page.get_site.root_page %}
|
{% with site_root=page.get_site.root_page %}
|
||||||
{# Top-level menu: direct children of site root #}
|
{# Top-level menu: direct children of site root #}
|
||||||
|
<li>
|
||||||
|
<a href="#">
|
||||||
|
最新文章
|
||||||
|
</a>
|
||||||
|
{% if nav_latest_page or nav_trending_page %}
|
||||||
|
<ul class="submenu">
|
||||||
|
{% if nav_latest_page %}
|
||||||
|
<li><a href="{{ nav_latest_page.url }}">{{ nav_latest_page.title }}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if nav_trending_page %}
|
||||||
|
<li><a href="{{ nav_trending_page.url }}">{{ nav_trending_page.title }}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
{% for menu_page in site_root.get_children.live.in_menu %}
|
{% for menu_page in site_root.get_children.live.in_menu %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ menu_page.url }}">{{ menu_page.title }}</a>
|
<a href="{{ menu_page.url }}">{{ menu_page.title }}</a>
|
||||||
|
|||||||
@ -3,3 +3,4 @@ wagtail>=7.1,<7.2
|
|||||||
gunicorn
|
gunicorn
|
||||||
dj-database-url
|
dj-database-url
|
||||||
psycopg[binary]
|
psycopg[binary]
|
||||||
|
python-dotenv
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user