- {% firstof article.headline article.title article.opengraph.title %}
- {% if article.author_display_name and article.author_profile_url %}
- {{ article.opengraph.published_time|date:"DATE_FORMAT"}}
- {{ article.author_display_name }}
- {% elif article.author_display_name %}
- {{ article.opengraph.published_time|date:"DATE_FORMAT"}} {{ article.author_display_name }}
+ {% firstof article.headline article.title article.schema.title %}
+ {% if article.author %}
+ {{ article.date_published|date:"DATE_FORMAT"}}
+ {{ article.author.name }}
{% else %}
- {{ article.opengraph.published_time|date:"DATE_FORMAT"}}
+ {{ article.date_published|date:"DATE_FORMAT"}}
{% endif %}
{% opengraph_image article as img %} {% if img %}
+ src="{{img.large.url}}"
+ alt="{{img.alt_text}}"
+ height="{{img.large.height}}"
+ width="{{img.large.width}}"
+ />
{% endif %}
{{ article.body|safe }}
diff --git a/src/genericsite/templates/genericsite/blocks/article_list_album.html b/src/genericsite/templates/genericsite/blocks/article_list_album.html
index 692f705..7521335 100644
--- a/src/genericsite/templates/genericsite/blocks/article_list_album.html
+++ b/src/genericsite/templates/genericsite/blocks/article_list_album.html
@@ -1,4 +1,4 @@
-{% load genericsite i18n static thumbnail %}
+{% load genericsite i18n static %}
diff --git a/src/genericsite/templates/genericsite/blocks/article_list_blog.html b/src/genericsite/templates/genericsite/blocks/article_list_blog.html
index 6f94037..2af1f71 100644
--- a/src/genericsite/templates/genericsite/blocks/article_list_blog.html
+++ b/src/genericsite/templates/genericsite/blocks/article_list_blog.html
@@ -2,14 +2,12 @@
{% for article in object_list %}
- {% if article.author_display_name and article.author_profile_url %}
- {{ article.opengraph.published_time|date:"DATE_FORMAT"}}
- {{ article.author_display_name }}
- {% elif article.author_display_name %}
- {{ article.opengraph.published_time|date:"DATE_FORMAT"}} {{ article.author_display_name }}
+ >{% firstof article.headline article.title article.name %}
+ {% if article.author %}
+ {{ article.date_published|date:"DATE_FORMAT"}}
+ {{ article.author.name }}
{% else %}
- {{ article.opengraph.published_time|date:"DATE_FORMAT"}}
+ {{ article.date_published|date:"DATE_FORMAT"}}
{% endif %}
{{ article.excerpt|safe }}
diff --git a/src/genericsite/templates/genericsite/blocks/article_text.html b/src/genericsite/templates/genericsite/blocks/article_text.html
index f9ae59b..e310759 100644
--- a/src/genericsite/templates/genericsite/blocks/article_text.html
+++ b/src/genericsite/templates/genericsite/blocks/article_text.html
@@ -1,13 +1,11 @@
{% with object as article %}
- {% firstof article.headline article.title article.opengraph.title %}
- {% if article.author_display_name and article.author_profile_url %}
- {{ article.opengraph.published_time|date:"DATE_FORMAT"}}
- {{ article.author_display_name }}
- {% elif article.author_display_name %}
- {{ article.opengraph.published_time|date:"DATE_FORMAT"}} {{ article.author_display_name }}
+ {% firstof article.headline article.title article.schema.headline %}
+ {% if article.author %}
+ {{ article.date_published|date:"DATE_FORMAT"}}
+ {{ article.author.name }}
{% else %}
- {{ article.opengraph.published_time|date:"DATE_FORMAT"}}
+ {{ article.date_published|date:"DATE_FORMAT"}}
{% endif %}
{{ article.body|safe }}
diff --git a/src/genericsite/templates/genericsite/blocks/author_list_album.html b/src/genericsite/templates/genericsite/blocks/author_list_album.html
new file mode 100644
index 0000000..8d9230b
--- /dev/null
+++ b/src/genericsite/templates/genericsite/blocks/author_list_album.html
@@ -0,0 +1,36 @@
+{% load genericsite i18n static %}
+
+
+
+ {% for author in object_list %}
+
+ {% endfor %}
+
+
+
+ {% include "genericsite/includes/pagination.html" %}
+
+
+
+
diff --git a/src/genericsite/templates/genericsite/blocks/author_profile.html b/src/genericsite/templates/genericsite/blocks/author_profile.html
new file mode 100644
index 0000000..c9003e6
--- /dev/null
+++ b/src/genericsite/templates/genericsite/blocks/author_profile.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
{{ object.name }}
+
{{ object.full_bio|safe }}
+
+
\ No newline at end of file
diff --git a/src/genericsite/templates/genericsite/blocks/header_simple.html b/src/genericsite/templates/genericsite/blocks/header_simple.html
index d2b5447..fcb3f6f 100644
--- a/src/genericsite/templates/genericsite/blocks/header_simple.html
+++ b/src/genericsite/templates/genericsite/blocks/header_simple.html
@@ -13,3 +13,6 @@
{% endfor %}
+{% comment %}
+The simple header is a branded menu bar with no extra features.
+{% endcomment %}
\ No newline at end of file
diff --git a/src/genericsite/templates/genericsite/includes/link_list.html b/src/genericsite/templates/genericsite/includes/link_list.html
new file mode 100644
index 0000000..dae9af4
--- /dev/null
+++ b/src/genericsite/templates/genericsite/includes/link_list.html
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/src/genericsite/templatetags/genericsite.py b/src/genericsite/templatetags/genericsite.py
index d5f784f..1c775e2 100644
--- a/src/genericsite/templatetags/genericsite.py
+++ b/src/genericsite/templatetags/genericsite.py
@@ -2,7 +2,6 @@
from django.contrib.sites.shortcuts import get_current_site
from django.utils import timezone
from django.utils.html import format_html
-
from genericsite.models import Menu, SectionMenu, SiteVar
register = template.Library()
@@ -146,12 +145,12 @@ def opengraph_image(context, og):
``{% opengraph_image article as img %}``
"""
- if img := getattr(og, "og_image"):
+ if img := getattr(og, "share_image"):
return img
if hasattr(og, "image_set"):
if img := og.image_set.first():
return img
if hasattr(og, "section"):
- if img := og.section.og_image:
+ if img := og.section.share_image:
return img
return None
diff --git a/src/genericsite/urls.py b/src/genericsite/urls.py
index 3fbebdf..c078bf1 100644
--- a/src/genericsite/urls.py
+++ b/src/genericsite/urls.py
@@ -13,28 +13,31 @@
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
-from django.urls import include, path
+
+from django.urls import path
from django.views.generic import RedirectView
from genericsite import views as generic
urlpatterns = [
- path("tinymce/", include("tinymce.urls")),
- path("accounts/profile/", generic.ProfileView.as_view(), name="account_profile"),
- path(
- "images/recent.json",
- generic.TinyMCEImageListView.as_view(),
- name="tinymce_image_list",
- ),
- # Above pages are used for admin/publishing, not live site. These redirects are for
- # live site but cannot be statically generated. Users will have to deal with that.
path(
"
/feed/", RedirectView.as_view(pattern_name="section_feed")
),
path("feed/", RedirectView.as_view(pattern_name="site_feed")),
- # URLs below can be statically generated.
# Home page pagination needs to come before the other page patterns to match.
path("page_.html", generic.HomePageView.as_view(), name="home_paginated"),
+ path("author/", generic.AuthorListView.as_view(), name="author_list"),
+ path(
+ "author//index.rss", generic.AuthorFeed(), name="author_feed"
+ ),
+ path(
+ "author//page_.html",
+ generic.AuthorView.as_view(),
+ name="author_page_paginated",
+ ),
+ path(
+ "author//", generic.AuthorView.as_view(), name="author_page"
+ ),
path(
"/page_.html",
generic.SectionView.as_view(),
diff --git a/src/genericsite/views.py b/src/genericsite/views.py
index 256c9b4..0b27851 100644
--- a/src/genericsite/views.py
+++ b/src/genericsite/views.py
@@ -1,19 +1,15 @@
import typing as T
from django.apps import apps
-from django.conf import settings
-from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.sites.shortcuts import get_current_site
from django.contrib.syndication.views import Feed
-from django.http import HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils import timezone
from django.utils.feedgenerator import Rss201rev2Feed
-from django.views.generic import DetailView, ListView, TemplateView
-from easy_thumbnails.files import get_thumbnailer
+from django.views.generic import DetailView, ListView
-from genericsite.models import Article, HomePage, Image, Page, Section
+from genericsite.models import Article, Author, HomePage, Page, Section
######################################################################################
@@ -101,9 +97,9 @@ def get_context_data(self, **kwargs):
context["object"] = self.object
context["opengraph"] = self.object.opengraph
context["precontent_template"] = site.vars.get_value("list_precontent_template")
- context["content_template"] = (
- self.object.content_template or site.vars.get_value("list_content_template")
- )
+ context["content_template"] = getattr(
+ self.object, "content_template", None
+ ) or site.vars.get_value("list_content_template")
context["postcontent_template"] = site.vars.get_value(
"list_postcontent_template"
)
@@ -145,13 +141,13 @@ def get_queryset(self):
def get_template_names(self):
names = []
# Per Django convention, `template_name` on the View takes precedence
- if tname := getattr(self, "template_name"):
+ if tname := getattr(self, "template_name", None):
names.append(tname)
# Django's ListView doesn't account for an object overriding the template,
# so we need to do that ourselves.
- if self.object.base_template:
- names.append(self.object.base_template)
+ if base_template := getattr(self.object, "base_template", None):
+ names.append(base_template)
# Allow using the Django convention /_list.html
if hasattr(self.object_list, "model"):
@@ -177,6 +173,65 @@ class ArticleListView(OpenGraphListView):
model = Article
+######################################################################################
+class AuthorView(ArticleListView):
+ allow_empty: bool = True
+ object = None
+
+ def get_context_data(self, **kwargs) -> T.Dict[str, T.Any]:
+ context = super().get_context_data(**kwargs)
+ context["precontent_template"] = "genericsite/blocks/author_profile.html"
+ return context
+
+ def get_object(self):
+ return get_object_or_404(
+ Author.objects.filter(
+ site=get_current_site(self.request),
+ slug=self.kwargs["author_slug"],
+ )
+ )
+
+ def get_queryset(self):
+ return super().get_queryset().filter(author=self.get_object())
+
+
+######################################################################################
+class AuthorListView(ListView):
+ model = Author
+ object = None
+
+ def dispatch(self, request, *args, **kwargs):
+ self.object = self.get_object()
+ return super().dispatch(request, *args, **kwargs)
+
+ def get_context_data(self, **kwargs) -> T.Dict[str, T.Any]:
+ context = super().get_context_data(**kwargs)
+ context["content_template"] = "genericsite/blocks/author_list_album.html"
+ return context
+
+ def get_object(self):
+ site_name = self.request.site.vars.get_value("brand", self.request.site.name)
+ self.object = Page(
+ site=get_current_site(self.request),
+ title=f"Contributors to {site_name}",
+ description="Authors who have contributed to this site.",
+ date_published=timezone.now(),
+ )
+ return self.object
+
+ def get_template_names(self) -> T.List[str]:
+ names = super().get_template_names()
+
+ # Fall back to site default if set
+ var = self.object.site.vars
+ if site_default := var.get_value("base_template"):
+ names.append(site_default)
+
+ # Fall back to Genericsite default
+ names.append("genericsite/base.html")
+ return names
+
+
######################################################################################
class SectionView(ArticleListView):
allow_empty: bool = True
@@ -208,7 +263,7 @@ def get_object(self):
site=get_current_site(self.request),
admin_name="__DEBUG__",
title="Generic Site",
- published_time=timezone.now(),
+ date_published=timezone.now(),
)
return hp
@@ -220,20 +275,6 @@ def get_context_data(self, **kwargs):
return context
-######################################################################################
-class ProfileView(LoginRequiredMixin, TemplateView):
- template_name = "registration/profile.html"
-
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- if "allauth" in settings.INSTALLED_APPS: # use allauth views
- context["change_password_view"] = "account_change_password"
- else: # use django.contrib.auth views
- context["change_password_view"] = "password_change"
-
- return context
-
-
######################################################################################
# FEEDS AND APIS
######################################################################################
@@ -299,13 +340,16 @@ def item_link(self, item):
return item.get_absolute_url()
def item_author_name(self, item):
- return item.author_display_name
+ if item.author:
+ return item.author.name
+ else:
+ return item.site.vars.get_value("author_display_name")
def item_pubdate(self, item):
- return item.published_time
+ return item.date_published
def item_updateddate(self, item):
- return item.modified_time
+ return item.date_modified
def item_copyright(self, item):
return item.copyright_notice
@@ -330,19 +374,22 @@ def get_object(self, request, *args, **kwargs):
)
def title(self, obj):
- return obj.opengraph.title
+ return obj.title
def link(self, obj):
return obj.get_absolute_url()
def description(self, obj):
- return obj.opengraph.description
+ return obj.description
def feed_url(self, obj):
return reverse("section_feed", kwargs={"section_slug": obj.slug})
def author_name(self, obj):
- return obj.author_display_name or obj.site.vars.get_value("author_display_name")
+ if obj.author:
+ return obj.author.name
+ else:
+ return obj.site.vars.get_value("author_display_name")
def feed_copyright(self, obj):
return obj.copyright_notice
@@ -353,32 +400,33 @@ def items(self, obj):
######################################################################################
-class TinyMCEImageListView(ListView):
- """This view provides an image list for the TinyMCE editor for easy insertion."""
-
- model = Image
- ordering = "-uploaded_dt"
- paginate_by = 25
-
- def render_to_response(
- self, context: T.Dict[str, T.Any], **response_kwargs: T.Any
- ) -> HttpResponse:
- images = context.get("page_obj")
- if images is None:
- images = context.get("object_list")
-
- def preset(i):
- if i.image_width < i.image_height:
- return "portrait_large"
- return "large"
-
- return JsonResponse(
- [
- {
- "title": i.title,
- "value": get_thumbnailer(i.image_file)[preset(i)].url,
- }
- for i in images
- ],
- safe=False,
+class AuthorFeed(SiteFeed):
+ "Feed of Articles by a specified author"
+
+ def get_object(self, request, *args, **kwargs):
+ "Return the Author for this feed"
+ return get_object_or_404(
+ Author.objects.filter(site=request.site, slug=kwargs["author_slug"])
)
+
+ def title(self, obj):
+ return obj.name
+
+ def link(self, obj):
+ return obj.get_absolute_url()
+
+ def description(self, obj):
+ return obj.description
+
+ def feed_url(self, obj):
+ return reverse("author_feed", kwargs={"author_slug": obj.slug})
+
+ def author_name(self, obj):
+ return obj.name
+
+ def feed_copyright(self, obj):
+ return obj.copyright_notice
+
+ def items(self, obj):
+ paginate_by = obj.site.vars.get_value("paginate_by", 15, asa=int)
+ return Article.objects.live().filter(author=obj)[:paginate_by]
diff --git a/src/genericsite/views_optional.py b/src/genericsite/views_optional.py
new file mode 100644
index 0000000..460021a
--- /dev/null
+++ b/src/genericsite/views_optional.py
@@ -0,0 +1,54 @@
+import typing as T
+
+from django.conf import settings
+from django.contrib.auth.mixins import LoginRequiredMixin
+from django.http import HttpResponse, JsonResponse
+from django.views.generic import ListView, TemplateView
+
+from genericsite.models import Image
+
+
+######################################################################################
+class ProfileView(LoginRequiredMixin, TemplateView):
+ template_name = "registration/profile.html"
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ if "allauth" in settings.INSTALLED_APPS: # use allauth views
+ context["change_password_view"] = "account_change_password"
+ else: # use django.contrib.auth views
+ context["change_password_view"] = "password_change"
+
+ return context
+
+
+######################################################################################
+class TinyMCEImageListView(ListView):
+ """This view provides an image list for the TinyMCE editor for easy insertion."""
+
+ model = Image
+ ordering = "-uploaded_dt"
+ paginate_by = 25
+
+ def render_to_response(
+ self, context: T.Dict[str, T.Any], **response_kwargs: T.Any
+ ) -> HttpResponse:
+ images = context.get("page_obj")
+ if images is None:
+ images = context.get("object_list")
+
+ def preset(i):
+ if i.image_width < i.image_height:
+ return "portrait_large"
+ return "large"
+
+ return JsonResponse(
+ [
+ {
+ "title": i.title,
+ # "value": get_thumbnailer(i.image_file)[preset(i)].url,
+ }
+ for i in images
+ ],
+ safe=False,
+ )
diff --git a/test_project/settings.py b/test_project/settings.py
index 06c69a9..c29f57d 100644
--- a/test_project/settings.py
+++ b/test_project/settings.py
@@ -9,10 +9,10 @@
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.0/ref/settings/
"""
+
from pathlib import Path
import environ
-
import genericsite.apps
# Build paths inside the project like this: BASE_DIR / 'subdir'.
@@ -35,19 +35,38 @@
DEBUG = True
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
-ALLOWED_HOSTS = []
+ALLOWED_HOSTS = ["127.0.0.1", "localhost"]
SITE_ID = env("SITE_ID", cast=int, default=None)
LOGIN_REDIRECT_URL = "/"
# Application definition
-INSTALLED_APPS = genericsite.apps.plus(
+INSTALLED_APPS = [
+ # genericsite apps for static site generation
+ *genericsite.apps.CONTENT,
+ # genericsite apps for publishing tools
"django_extensions",
"allauth",
"allauth.account",
"allauth.socialaccount",
-)
+ "tinymce",
+ # contrib apps required by genericsite for statics
+ "django.contrib.contenttypes",
+ "django.contrib.redirects",
+ "django.contrib.sitemaps",
+ "django.contrib.sites",
+ "django.contrib.staticfiles",
+ # contrib apps required by genericsite for dynamically served apps
+ "django.contrib.auth",
+ "django.contrib.messages",
+ "django.contrib.sessions",
+ # Optional admin with genericsite extensions
+ "django.contrib.admin",
+ "django.contrib.admindocs",
+ *genericsite.apps.ADMIN,
+]
+# Note: most of these middlewares are not required for static generation
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
@@ -84,6 +103,15 @@
},
]
+STORAGES = {
+ "default": {
+ "BACKEND": "django.core.files.storage.FileSystemStorage",
+ },
+ "staticfiles": {
+ "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
+ },
+}
+
WSGI_APPLICATION = "test_project.wsgi.application"
@@ -127,10 +155,10 @@
CONN_HEALTH_CHECKS = True
-THUMBNAIL_PROCESSORS = genericsite.apps.THUMBNAIL_PROCESSORS
-THUMBNAIL_DEBUG = DEBUG
+# THUMBNAIL_PROCESSORS = genericsite.apps.THUMBNAIL_PROCESSORS
+# THUMBNAIL_DEBUG = DEBUG
-TINYMCE_DEFAULT_CONFIG = genericsite.apps.TINYMCE_CONFIG
+# TINYMCE_DEFAULT_CONFIG = genericsite.apps.TINYMCE_CONFIG
#######################################################################
# DEVELOPMENT: If running in a dev environment, loosen restrictions
@@ -140,7 +168,9 @@
ALLOWED_HOSTS = ["*"]
# Use the basic storage with no manifest
- STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage"
+ STORAGES["staticfiles"]["BACKEND"] = (
+ "django.contrib.staticfiles.storage.StaticFilesStorage"
+ )
try:
import debug_toolbar
diff --git a/test_project/urls.py b/test_project/urls.py
index ad6a5c2..a9f4e41 100644
--- a/test_project/urls.py
+++ b/test_project/urls.py
@@ -13,17 +13,30 @@
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
+
from django.conf import settings
from django.contrib import admin
+from django.contrib.sitemaps.views import sitemap
from django.urls import include, path
-
-from genericsite import views as generic
+from genericsite import views_optional as optional
+from genericsite.sitemaps import sitemaps
urlpatterns = [
- path("accounts/profile/", generic.ProfileView.as_view(), name="account_profile"),
+ path(
+ "images/recent.json",
+ optional.TinyMCEImageListView.as_view(),
+ name="tinymce_image_list",
+ ),
+ path("accounts/profile/", optional.ProfileView.as_view(), name="account_profile"),
path("accounts/", include("allauth.urls")),
path("django_accounts/", include("django.contrib.auth.urls")),
path("admin/", admin.site.urls),
+ path(
+ "sitemap.xml",
+ sitemap,
+ {"sitemaps": sitemaps},
+ name="django.contrib.sitemaps.views.sitemap",
+ ),
path("", include("genericsite.urls")),
]
if settings.DEBUG:
diff --git a/tests/test_models.py b/tests/test_models.py
index 0e4127b..d7621b3 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -1,7 +1,6 @@
from datetime import datetime
from django.test import TestCase as DjangoTestCase
-
from genericsite.models import Page, Site, SiteVar, Status
@@ -16,7 +15,7 @@ def test_copyright_notice_has_custom(self):
slug="test-page",
status=Status.USABLE,
site=site,
- published_time=datetime.fromisoformat("2021-11-22T19:00"),
+ date_published=datetime.fromisoformat("2021-11-22T19:00"),
custom_copyright_notice="{} custom copyright notice",
)
assert "2021 custom copyright notice" in page.copyright_notice
@@ -35,7 +34,7 @@ def test_copyright_notice_site_has_fallback(self):
slug="test-page",
status=Status.USABLE,
site=site,
- published_time=datetime.fromisoformat("2021-11-22T19:00"),
+ date_published=datetime.fromisoformat("2021-11-22T19:00"),
)
assert f"2021 sitewide copyright" in page.copyright_notice
@@ -53,7 +52,7 @@ def test_copyright_notice_site_has_holder(self):
slug="test-page",
status=Status.USABLE,
site=site,
- published_time=datetime.fromisoformat("2021-11-22T19:00"),
+ date_published=datetime.fromisoformat("2021-11-22T19:00"),
)
assert f"2021 custom holder. All rights" in page.copyright_notice
@@ -68,7 +67,7 @@ def test_copyright_notice_default(self):
slug="test-page",
status=Status.USABLE,
site=site,
- published_time=datetime.fromisoformat("2021-11-22T19:00"),
+ date_published=datetime.fromisoformat("2021-11-22T19:00"),
)
assert f"2021 example.com. All rights" in page.copyright_notice
@@ -82,7 +81,7 @@ def test_explicit_excerpt(self):
slug="test-page",
status=Status.USABLE,
site=site,
- published_time=datetime.fromisoformat("2021-11-22T19:00"),
+ date_published=datetime.fromisoformat("2021-11-22T19:00"),
body="""First paragraph.
Second paragraph.
@@ -99,7 +98,7 @@ def test_no_explicit_excerpt(self):
slug="test-page",
status=Status.USABLE,
site=site,
- published_time=datetime.fromisoformat("2021-11-22T19:00"),
+ date_published=datetime.fromisoformat("2021-11-22T19:00"),
body="""First paragraph.
Second paragraph.
""",
diff --git a/tests/test_schemas.py b/tests/test_schemas.py
index cdb5c69..8ddd111 100644
--- a/tests/test_schemas.py
+++ b/tests/test_schemas.py
@@ -1,63 +1,89 @@
+import datetime
+import unittest
+
from genericsite.schemas import AudioProp, ImageProp, OGArticle, OGProfile
-def test_audio_schema():
- url = "https://example.com/audio.mp3"
- a = AudioProp(url=url, secure_url=url, type="audio/mp3")
- print(a)
- assert "_prefix" not in a.__str__()
- assert "audio:url" not in a.__str__()
- assert '''property="og:audio"''' in a.__str__()
- assert "og:audio:secure_url" in a.__str__()
- assert "og:audio:type" in a.__str__()
+class TestOpenGraphSchemas(unittest.TestCase):
+ def test_audio_schema(self):
+ url = "https://example.com/audio.mp3"
+ a = AudioProp(url=url, secure_url=url, type="audio/mp3")
+ self.assertNotIn("_prefix", a.__str__())
+ self.assertNotIn("audio:url", a.__str__())
+ self.assertIn('property="og:audio"', a.__str__())
+ self.assertIn("og:audio:secure_url", a.__str__())
+ self.assertIn("og:audio:type", a.__str__())
+ def test_article_schema(self):
+ a = OGArticle(
+ title="My First Article",
+ description="An article about the first opengraph object",
+ url="https://example.com/",
+ published_time="2022-06-30T12:00:00Z",
+ author=["https://vince.veselosky.me"],
+ tag=["testpost", "ignoreme"],
+ )
+ self.assertIn('property="og:type" content="article"', str(a))
+ self.assertIn('property="article:tag" content="testpost"', str(a))
+ self.assertIn('property="article:tag" content="ignoreme"', str(a))
+ self.assertIn(
+ 'property="article:author" content="https://vince.veselosky.me"',
+ str(a),
+ )
+ self.assertIn("2022-06-30T12:00:00Z", str(a))
-def test_article_schema():
- a = OGArticle(
- title="My First Article",
- description="An article about the first opengraph object",
- url="https://example.com/",
- published_time="2022-06-30T12:00:00Z",
- author=["https://vince.veselosky.me"],
- tag=["testpost", "ignoreme"],
- )
- print(a)
- assert '''property="og:type" content="article"''' in str(a)
- assert '''property="article:tag" content="testpost"''' in str(a)
- assert '''property="article:tag" content="ignoreme"''' in str(a)
- assert '''property="article:author" content="https://vince.veselosky.me"''' in str(
- a
- )
- assert "2022-06-30 12:00:00+00:00" in str(a)
+ def test_article_schema_with_image(self):
+ img = ImageProp(
+ url="https://example.com/image.jpg",
+ type="image/jpeg",
+ width=1280,
+ height=720,
+ alt="alt text here",
+ )
+ a = OGArticle(
+ title="Article With Image",
+ description="An article about the first opengraph object",
+ url="https://example.com/",
+ published_time="2022-06-30T12:00:00Z",
+ author=["https://vince.veselosky.me"],
+ tag=["testpost", "ignoreme"],
+ image=[img],
+ )
+ self.assertIn('property="og:type" content="article"', str(a))
+ self.assertIn('property="article:tag" content="testpost"', str(a))
+ self.assertIn('property="article:tag" content="ignoreme"', str(a))
+ self.assertIn(
+ 'property="article:author" content="https://vince.veselosky.me"',
+ str(a),
+ )
+ self.assertIn("2022-06-30T12:00:00Z", str(a))
+ self.assertIn(
+ 'property="og:image" content="https://example.com/image.jpg"', str(a)
+ )
+ self.assertIn('property="og:image:type" content="image/jpeg"', str(a))
+ self.assertIn('property="og:image:width" content="1280"', str(a))
+ self.assertIn('property="og:image:height" content="720"', str(a))
+ self.assertIn('property="og:image:alt" content="alt text here"', str(a))
+ def test_article_schema_using_datetime(self):
+ a = OGArticle(
+ title="Article With datetime object",
+ description="An article about the first opengraph object",
+ url="https://example.com/",
+ published_time=datetime.datetime(2022, 6, 30, 12, 0, 0),
+ author=["https://vince.veselosky.me"],
+ tag=["testpost", "ignoreme"],
+ )
+ self.assertIn("2022-06-30T12:00:00", str(a))
-def test_article_schema_with_image():
- img = ImageProp(
- url="https://example.com/image.jpg",
- type="image/jpeg",
- width=1280,
- height=720,
- alt="alt text here",
- )
- a = OGArticle(
- title="My First Article",
- description="An article about the first opengraph object",
- url="https://example.com/",
- published_time="2022-06-30T12:00:00Z",
- author=["https://vince.veselosky.me"],
- tag=["testpost", "ignoreme"],
- image=[img],
- )
- print(a)
- assert '''property="og:type" content="article"''' in str(a)
- assert '''property="article:tag" content="testpost"''' in str(a)
- assert '''property="article:tag" content="ignoreme"''' in str(a)
- assert '''property="article:author" content="https://vince.veselosky.me"''' in str(
- a
- )
- assert "2022-06-30 12:00:00+00:00" in str(a)
- assert 'property="og:image" content="https://example.com/image.jpg"' in str(a)
- assert 'property="og:image:type" content="image/jpeg"' in str(a)
- assert 'property="og:image:width" content="1280"' in str(a)
- assert 'property="og:image:height" content="720"' in str(a)
- assert 'property="og:image:alt" content="alt text here"' in str(a)
+ def test_article_schema_using_date(self):
+ a = OGArticle(
+ title="Article With date object",
+ description="An article about the first opengraph object",
+ url="https://example.com/",
+ published_time=datetime.date(2022, 6, 30),
+ author=["https://vince.veselosky.me"],
+ tag=["testpost", "ignoreme"],
+ )
+ # When set to a date, value is converted to isoformat
+ self.assertIn("2022-06-30", str(a))
diff --git a/tests/test_templatetags.py b/tests/test_templatetags.py
index 1fc0641..f2f72ee 100644
--- a/tests/test_templatetags.py
+++ b/tests/test_templatetags.py
@@ -62,15 +62,14 @@ def test_copyright_notice_obj_has_custom(self):
slug="test-page",
status=Status.USABLE,
site=site,
- published_time=datetime.fromisoformat("2021-11-22T19:00"),
+ date_published=datetime.fromisoformat("2021-11-22T19:00"),
custom_copyright_notice="{} custom copyright notice",
)
output = Template("{% load genericsite %}{% copyright_notice %} ").render(
Context({"object": page})
)
- print(output)
- assert page.copyright_notice in output
- assert "2021 custom copyright notice" in output
+ self.assertIn(page.copyright_notice, output)
+ self.assertIn("2021 custom copyright notice", output)
def test_copyright_notice_site_has_fallback(self):
"""Context contains an object that has no copyright_notice prop.
@@ -87,8 +86,8 @@ def test_copyright_notice_site_has_fallback(self):
output = Template("{% load genericsite %}{% copyright_notice %} ").render(
Context({"request": request, "object": object()})
)
- print(output)
- assert f"{year} sitewide copyright" in output
+
+ self.assertIn(f"{year} sitewide copyright", output)
def test_copyright_notice_site_has_holder(self):
"""Context contains an object that has no copyright_notice prop.
@@ -105,8 +104,8 @@ def test_copyright_notice_site_has_holder(self):
output = Template("{% load genericsite %}{% copyright_notice %} ").render(
Context({"request": request, "object": object()})
)
- print(output)
- assert f"{year} custom holder. All rights" in output
+
+ self.assertIn(f"{year} custom holder. All rights", output)
def test_copyright_notice_site_default(self):
"""Context contains an object that has no copyright_notice prop.
@@ -119,5 +118,4 @@ def test_copyright_notice_site_default(self):
output = Template("{% load genericsite %}{% copyright_notice %} ").render(
Context({"request": request, "object": object()})
)
- print(output)
- assert f"{year} example.com. All rights" in output
+ self.assertIn(f"{year} example.com. All rights", output)
diff --git a/tests/test_views.py b/tests/test_views.py
index 49c63d5..21316f6 100644
--- a/tests/test_views.py
+++ b/tests/test_views.py
@@ -27,7 +27,7 @@ def test_homepage_exists(self):
site=site,
admin_name="Test HomePage",
title="Test HomePage 1",
- published_time=timezone.now(),
+ date_published=timezone.now(),
)
resp = self.client.get(reverse("home_page"))
self.assertEqual(resp.status_code, 200)
@@ -43,13 +43,13 @@ def test_homepage_site_filter(self):
site=site,
admin_name="Test HomePage",
title="Test HomePage 1",
- published_time=timezone.now() - timedelta(days=1),
+ date_published=timezone.now() - timedelta(days=1),
)
hp2 = HomePage.objects.create(
site=site2,
admin_name="NOT MY HomePage",
title="NOT MY HomePage",
- published_time=timezone.now(),
+ date_published=timezone.now(),
)
resp = self.client.get(reverse("home_page"))
self.assertEqual(resp.status_code, 200)
@@ -62,13 +62,13 @@ def test_draft_homepage_skipped(self):
site=site,
admin_name="Test HomePage",
title="Test HomePage 1",
- published_time=timezone.now() - timedelta(days=1),
+ date_published=timezone.now() - timedelta(days=1),
)
hp_draft = HomePage.objects.create(
site=site,
admin_name="DRAFT HomePage",
title="DRAFT HomePage 1",
- published_time=timezone.now(),
+ date_published=timezone.now(),
status=Status.WITHHELD,
)
# Draft is newer and would be selected if not filtering on status.
@@ -83,13 +83,13 @@ def test_future_homepage_skipped(self):
site=site,
admin_name="Test HomePage",
title="Test HomePage 1",
- published_time=timezone.now() - timedelta(days=1),
+ date_published=timezone.now() - timedelta(days=1),
)
hp_tomorrow = HomePage.objects.create(
site=site,
admin_name="FUTURE HomePage",
title="FUTURE HomePage 1",
- published_time=timezone.now() + timedelta(days=1),
+ date_published=timezone.now() + timedelta(days=1),
)
# HP scheduled for tomorrow should not be selected today
resp = self.client.get(reverse("home_page"))
@@ -110,7 +110,7 @@ def test_draft_page(self):
site=site,
slug="test-page",
title="Test Page 1",
- published_time=timezone.now(),
+ date_published=timezone.now(),
status=Status.WITHHELD,
)
resp = self.client.get(
@@ -124,7 +124,7 @@ def test_future_page(self):
site=site,
slug="test-page",
title="Test Page 1",
- published_time=timezone.now() + timedelta(days=1),
+ date_published=timezone.now() + timedelta(days=1),
)
resp = self.client.get(
reverse("landing_page", kwargs={"page_slug": "test-page"})
@@ -140,7 +140,7 @@ def test_page_site_filter(self):
site=site2,
slug="test-page",
title="Test Page 1",
- published_time=timezone.now(),
+ date_published=timezone.now(),
)
resp = self.client.get(
reverse("landing_page", kwargs={"page_slug": "test-page"})
@@ -153,7 +153,7 @@ def test_page(self):
site=site,
slug="test-page",
title="Test Page 1",
- published_time=timezone.now(),
+ date_published=timezone.now(),
)
resp = self.client.get(
reverse("landing_page", kwargs={"page_slug": "test-page"})
@@ -172,7 +172,7 @@ def test_empty_section(self):
site=site,
slug="test-section",
title="Test Section 1",
- published_time=timezone.now(),
+ date_published=timezone.now(),
)
resp = self.client.get(
reverse("section_page", kwargs={"section_slug": "test-section"})
@@ -192,7 +192,7 @@ def test_draft_section(self):
site=site,
slug="test-section",
title="Test Section 1",
- published_time=timezone.now(),
+ date_published=timezone.now(),
status=Status.WITHHELD,
)
resp = self.client.get(
@@ -206,7 +206,7 @@ def test_future_section(self):
site=site,
slug="test-section",
title="Test Section 1",
- published_time=timezone.now() + timedelta(days=1),
+ date_published=timezone.now() + timedelta(days=1),
)
resp = self.client.get(
reverse("section_page", kwargs={"section_slug": "test-section"})
@@ -222,7 +222,7 @@ def test_section_site_filter(self):
site=site2,
slug="test-section",
title="Test Section 1",
- published_time=timezone.now(),
+ date_published=timezone.now(),
)
resp = self.client.get(
reverse("section_page", kwargs={"section_slug": "test-section"})
@@ -261,33 +261,33 @@ def setUpTestData(cls):
site=site,
title="Test Home Page",
slug="test-home-page",
- published_time=timezone.now(),
+ date_published=timezone.now(),
)
section = Section.objects.create(
site=site,
slug="test-section",
title="Test Section 1",
- published_time=timezone.now(),
+ date_published=timezone.now(),
)
section2 = Section.objects.create(
site=site2,
slug="test-section",
title="Test Section 2",
- published_time=timezone.now(),
+ date_published=timezone.now(),
)
article = Article.objects.create(
site=site,
slug="test-article",
section=section,
title="Test Article 1",
- published_time=timezone.now() - timedelta(days=1),
+ date_published=timezone.now() - timedelta(days=1),
)
article2 = Article.objects.create(
site=site2,
slug="test-article",
section=section2,
title="Test Article 2",
- published_time=timezone.now(),
+ date_published=timezone.now(),
)
cls.site = site
cls.site2 = site2
@@ -319,7 +319,7 @@ def test_article_draft(self):
slug="draft-article",
section=self.section,
title="DRAFT Article 1",
- published_time=timezone.now(),
+ date_published=timezone.now(),
status=Status.WITHHELD,
)
resp = self.client.get(
@@ -340,7 +340,7 @@ def test_article_future(self):
slug="future-article",
section=self.section,
title="FUTURE Article 1",
- published_time=timezone.now() + timedelta(days=1),
+ date_published=timezone.now() + timedelta(days=1),
)
resp = self.client.get(
reverse(
diff --git a/tox.ini b/tox.ini
index 4c52761..5b133a6 100644
--- a/tox.ini
+++ b/tox.ini
@@ -2,7 +2,7 @@
# We support Python 3.8 and later.
# We test against all currently supported Django versions.
envlist =
- {py38,py39,py310,py311,py312}-django{32,42}
+ {py38,py39,py310,py311,py312}-django{42}
{py310,py311,py312}-django{50}
py38-checks
#skipsdist = true
@@ -20,7 +20,6 @@ commands =
pytest {toxinidir}/tests
python manage.py validate_templates --ignore-app allauth
deps =
- django32: Django>=3.2,<3.3
django40: Django>=4.0,<4.1
django41: Django>=4.1,<4.2
django42: Django>=4.2,<4.3
@@ -30,27 +29,28 @@ deps =
django-environ
pytest
pytest-django
+ django-tinymce
[testenv:py38-checks]
basepython=python3.8
usedevelop=true
commands =
- ; Check code style
- pycodestyle {toxinidir}/src {toxinidir}/tests
+ ; ; Check code style
+ ; pycodestyle {toxinidir}/src {toxinidir}/tests
; Check whether you forgot to run makemigrations after changing models
python manage.py makemigrations --no-input --dry-run --check
; Generate test coverage report
- pytest --cov=src --cov-report html --cov-config=tox.ini tests
+ ; pytest --cov=src --cov-report html --cov-config=tox.ini tests
deps =
Django>=3.2,<3.3
django-allauth >= 0.51.0 # For testing allauth templates
coverage
django-environ
django_extensions
- pycodestyle
pytest
pytest-cov
pytest-django
+ django-tinymce
[coverage:run]
# Measure branch coverage as well as statement coverage
@@ -62,11 +62,6 @@ omit =
**/migrations/*
tests/**
-[pycodestyle]
-exclude = migrations
-ignore = E203, E501, W503
-max-line-length = 88
-
[pytest]
DJANGO_SETTINGS_MODULE = test_project.settings
python_files = tests.py test_*.py *_test.py *_tests.py