3 | {% include "cast/wagtail/_file_field.html" %}
4 |
5 |
--------------------------------------------------------------------------------
/docs/releases/0.2.44.rst:
--------------------------------------------------------------------------------
1 | 0.2.44 (2025-03-09)
2 | -------------------
3 |
4 | Get transcript models via repository.
5 |
6 | - #168 transcript models for feed are now contained in repository and don't need to be fetched from the database.
7 | - #179 better documentation on how to run the tests
8 |
--------------------------------------------------------------------------------
/src/cast/templates/cast/bootstrap4/403_csrf.html:
--------------------------------------------------------------------------------
1 | {% extends "./base.html" %}
2 |
3 | {% block title %}Forbidden (403){% endblock %}
4 |
5 | {% block content %}
6 |
10 | {% endfor %}
11 | {% endblock content %}
12 |
--------------------------------------------------------------------------------
/docs/releases/0.2.29.rst:
--------------------------------------------------------------------------------
1 | 0.2.29 (2024-03-23)
2 | -------------------
3 |
4 | Bugfixes:
5 |
6 | - #122 Sometimes galleries had no id -> re-publishing posts with those galleries fixed this
7 | - #124 Implicit feed caching caused delivering sometimes the wrong feed
8 | - #125 Use absolute link in feed when linking to a post (podcast clients break on relative links)
9 |
--------------------------------------------------------------------------------
/docs/releases/0.2.0.rst:
--------------------------------------------------------------------------------
1 | 0.2.0 (2022-12-18)
2 | ------------------
3 |
4 | This release took a long time. I can't even remember when exactly I started working on this. Some years ago I guess 😁.
5 |
6 | * CMS based on wagtail instead of custom template tags
7 | * Using flit instead of poetry
8 | * Updates of bootstrap / jquery etc
9 | * Docs based on furo theme
10 |
--------------------------------------------------------------------------------
/src/cast/comments/apps.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from django.apps import AppConfig
4 |
5 |
6 | class CastCommentsConfig(AppConfig):
7 | name = "cast.comments"
8 | label = "cast_comments"
9 |
10 | def ready(self) -> None:
11 | # Register signal receivers.
12 | from . import receivers as _receivers # noqa: F401
13 |
--------------------------------------------------------------------------------
/src/cast/templates/cast/video_form.html:
--------------------------------------------------------------------------------
1 | {% extends "cast_base.html" %}
2 | {% load static %}
3 |
4 | {% block content %}
5 |
6 |
11 | {% endblock content %}
12 |
--------------------------------------------------------------------------------
/src/cast/templates/cast/image_form.html:
--------------------------------------------------------------------------------
1 | {% extends "cast/cast_base.html" %}
2 | {% load static %}
3 |
4 | {% block content %}
5 |
6 |
11 | {% endblock content %}
12 |
--------------------------------------------------------------------------------
/docs/releases/0.2.28.rst:
--------------------------------------------------------------------------------
1 | 0.2.28 (2024-03-17)
2 | -------------------
3 |
4 | Feed performance improvements.
5 |
6 | - #118 Fix performance for feeds with many items / images
7 | - #121 Add support for `Wagtail 6 `_.
8 | - dropped support for Wagtail 4 and Django < 4.2
9 | - some pre-commit and javascript updates
10 |
--------------------------------------------------------------------------------
/docs/operations/troubleshooting.rst:
--------------------------------------------------------------------------------
1 | .. _troubleshooting_overview:
2 |
3 | ***************
4 | Troubleshooting
5 | ***************
6 |
7 | This section will provide solutions to common issues when working with Django Cast.
8 |
9 | .. note::
10 |
11 | Troubleshooting documentation is currently being developed. Please check back later for detailed troubleshooting guides.
12 |
--------------------------------------------------------------------------------
/tests/meta_test.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from django.urls import reverse
3 |
4 |
5 | @pytest.mark.django_db
6 | def test_twitter_player_blog_does_not_match(client, blog, episode):
7 | assert blog != episode.blog
8 | url = reverse("cast:twitter-player", kwargs={"blog_slug": blog.slug, "episode_slug": episode.slug})
9 | r = client.get(url)
10 | assert r.status_code == 404
11 |
--------------------------------------------------------------------------------
/docs/releases/0.2.4.rst:
--------------------------------------------------------------------------------
1 | 0.2.4 (2023-02-06)
2 | ------------------
3 |
4 | Split blog and podcast models.
5 |
6 | * Some documentation enhancements/fixes
7 | * Removed pub_date field from Post model (this is now handled by wagtail)
8 | * Removed inheriting from TimestampedModel in Post and Blog models
9 | * Split blog and podcast models + split index pages and detail pages into different modules
10 |
--------------------------------------------------------------------------------
/src/cast/templates/cast/plain/500.html:
--------------------------------------------------------------------------------
1 | {% extends "./base.html" %}
2 |
3 | {% block title %}Server Error{% endblock %}
4 |
5 | {% block content %}
6 |
Ooops!!! 500
7 |
8 |
Looks like something went wrong!
9 |
10 |
We track these errors automatically, but if the problem persists feel free to contact us. In the meantime, try refreshing.
11 | {% endblock content %}
12 |
--------------------------------------------------------------------------------
/docs/media/video.rst:
--------------------------------------------------------------------------------
1 | .. _video_overview:
2 |
3 | *****
4 | Video
5 | *****
6 |
7 | You can upload video files to the server and play them back in the browser.
8 |
9 | Videos have a `title` a file field `original` pointing to the original video
10 | file, an image `poster` and a list of `tags`.
11 |
12 | If no `poster` is given, the first frame of the video after one second is used
13 | as poster.
14 |
--------------------------------------------------------------------------------
/docs/releases/0.1/0.1.24.rst:
--------------------------------------------------------------------------------
1 | 0.1.24 (2019-05-22)
2 | -------------------
3 |
4 | * Use blog.email as itunes:email instead of blog.user.email
5 | * Added author field to have user editable author name
6 | * Translation should now work since locale dir is included in MANIFEST.in
7 | * Include documentation in package
8 | * Use visible_date as pubDate for feed and sort feed by -visible_date instead of -pub_date
9 |
--------------------------------------------------------------------------------
/src/cast/templates/cast/bootstrap4/500.html:
--------------------------------------------------------------------------------
1 | {% extends "./base.html" %}
2 |
3 | {% block title %}Server Error{% endblock %}
4 |
5 | {% block content %}
6 |
Ooops!!! 500
7 |
8 |
Looks like something went wrong!
9 |
10 |
We track these errors automatically, but if the problem persists feel free to contact us. In the meantime, try refreshing.
{% trans "Are you sure you want to delete this transcript?" %}
10 |
14 |
15 | {% endblock %}
16 |
--------------------------------------------------------------------------------
/docs/releases/0.2.30.rst:
--------------------------------------------------------------------------------
1 | 0.2.30 (2024-04-26)
2 | -------------------
3 |
4 | Render feed, blog index and post detail without hitting the database
5 | using only data from the respective repositories is now working. The
6 | repository is passed from the feed / blog / post models to the blocks
7 | just using the template context. And there's no need for monkeypatching
8 | the page link handler anymore, since a new page link handler is now
9 | set via the `register_rich_text_features` wagtail hook.
10 |
11 | - #123 Fixed an audio key error when a preview page is saved
12 | - #125 Add a hint (click to comment ) to feed descriptions of posts where comments are enabled
13 | - #128 Update vite + jsdom + new javascript build
14 | - #126 Render feed / blog index / post detail using only data from repository, not database
15 |
--------------------------------------------------------------------------------
/docs/releases/0.2.15.rst:
--------------------------------------------------------------------------------
1 | 0.2.15 (2023-05-22)
2 | -------------------
3 |
4 | Support for SPA themes and some htmx fixes
5 |
6 | * Expose data via template context to theme (for vue theme)
7 | * pagination page size
8 | * wagtail api page url via `reverse("cast:api:wagtail:pages:listing")`
9 | * facet count api base url
10 | * Add overview_html and detail_html to the Post model to get the rendered html in the vue theme
11 | * The template for image galleries can now be overwritten by themes
12 | * Fixed audio player on htmx pagination
13 | * Fixed galleries on htmx pagination
14 | * Don't remove newlines in ``*_html`` because it breaks preformatted code blocks
15 | * Combine wagtail api pages endpoint with django-filter to allow filtering by date facets and fulltext search
16 | * Add facet count api endpoints
17 |
--------------------------------------------------------------------------------
/docs/releases/0.2.3.rst:
--------------------------------------------------------------------------------
1 | 0.2.3 (2023-01-30)
2 | ------------------
3 |
4 | Split up post in episodes and posts.
5 |
6 | * Fixed [codecov](https://codecov.io) badge
7 | * More information about the release process, so I don't have to guess every time I publish a new release
8 | * Added mypy to pre-commit hooks and fixed some issues
9 | * Fixed Post form in django admin (don't try to save body stream field)
10 | * Fixed a little issue raising an exception when an unknown language is set for a code block
11 | * Split up post page model in episodes and posts
12 | * Renamed test files to make the names easier searchable
13 | * Use a JSONField for the audio model to cache file sizes
14 | * Removed to warnings showing up running migrations
15 | * Added twitter player card metadata to episode detail page + single button player view
16 |
--------------------------------------------------------------------------------
/src/cast/comments/templates/fluent_comments/templatetags/ajax_comment_tags.html:
--------------------------------------------------------------------------------
1 | {% load i18n static %}
2 | {% trans "cancel reply" %}
3 |
4 | {% trans "Please wait . . ." %}
5 |
6 | {% trans "Your comment has been posted!" %}
7 | {% if request.user.is_staff %}
8 |
9 | {% trans "Your comment has been posted, it will be visible for other users after approval." %}
10 |
11 | {% endif %}
12 |
--------------------------------------------------------------------------------
/bootstrap.md:
--------------------------------------------------------------------------------
1 | # Bootstrap Django-Cast
2 |
3 | ## Generate Hashes
4 |
5 | Development:
6 | ```shell
7 | python -m piptools compile --upgrade --allow-unsafe --generate-hashes requirements/production.in requirements/develop.in --output-file requirements/develop.txt
8 | ```
9 |
10 | Production:
11 |
12 | ```shell
13 | python -m piptools compile --upgrade --allow-unsafe --generate-hashes requirements/production.in --output-file requirements/production.txt
14 | ```
15 |
16 | ## Install Requirements
17 |
18 | ```shell
19 | python -m piptools sync requirements/develop.txt
20 | ```
21 |
22 | ## Install Cast Package
23 | ```shell
24 | python -m pip install -e .
25 | ```
26 |
27 | ## Get Example app running
28 |
29 | ```shell
30 | python manage.py migrate
31 | ```
32 |
33 | ```shell
34 | python manage.py runserver 0.0.0.0:8000
35 | ```
36 |
--------------------------------------------------------------------------------
/src/cast/migrations/0034_remove_old_podcast_fields_from_blog.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.4 on 2023-02-04 13:59
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("cast", "0033_add_new_podcast_model"),
10 | ]
11 |
12 | operations = [
13 | migrations.RemoveField(
14 | model_name="blog",
15 | name="explicit",
16 | ),
17 | migrations.RemoveField(
18 | model_name="blog",
19 | name="itunes_artwork",
20 | ),
21 | migrations.RemoveField(
22 | model_name="blog",
23 | name="itunes_categories",
24 | ),
25 | migrations.RemoveField(
26 | model_name="blog",
27 | name="keywords",
28 | ),
29 | ]
30 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [flake8]
2 | statistics = True
3 | ignore = D203,W503
4 | exclude =
5 | cast/migrations,
6 | .git,
7 | .tox,
8 | docs_old/conf.py,
9 | build,
10 | threadedcomments/*,
11 | dist
12 | max-line-length = 119
13 |
14 | [isort]
15 | known_first_party=cast
16 | known_django=django
17 | known_wagtail=wagtail,modelcluster
18 | skip=migrations,.git,__pycache__,LC_MESSAGES,locale,build,dist,.github,wagtail,threadedcomments
19 | blocked_extensions=rst,html,js,svg,txt,css,scss,png,snap,tsx,sh
20 | sections=FUTURE,STDLIB,DJANGO,WAGTAIL,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
21 | default_section=THIRDPARTY
22 | lines_between_types=1
23 | lines_after_imports=2
24 | multi_line_output=3
25 | include_trailing_comma=True
26 | force_grid_wrap=0
27 | use_parentheses=True
28 | ensure_newline_before_comments = True
29 | line_length = 88
30 |
--------------------------------------------------------------------------------
/runtests.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import os
4 | import shutil
5 | import sys
6 |
7 | from pathlib import Path
8 |
9 | import django
10 |
11 | from django.conf import settings
12 | from django.test.utils import get_runner
13 |
14 |
15 | def run_tests(*test_args):
16 | if not test_args:
17 | test_args = ["tests"]
18 |
19 | os.environ["DJANGO_SETTINGS_MODULE"] = "tests.settings"
20 | django.setup()
21 | TestRunner = get_runner(settings)
22 | test_runner = TestRunner()
23 | failures = test_runner.run_tests(test_args)
24 | media_root = Path(settings.MEDIA_ROOT)
25 | try:
26 | shutil.rmtree(media_root) # FIXME move cleanup to tests
27 | except FileNotFoundError:
28 | pass
29 | sys.exit(bool(failures))
30 |
31 |
32 | if __name__ == "__main__":
33 | run_tests(*sys.argv[1:])
34 |
--------------------------------------------------------------------------------
/src/cast/migrations/0004_homepage_alias_for_page.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.2 on 2020-10-24 06:07
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ("wagtailcore", "0052_pagelogentry"),
11 | ("cast", "0003_remove_post_parent_blog"),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name="homepage",
17 | name="alias_for_page",
18 | field=models.ForeignKey(
19 | blank=True,
20 | default=None,
21 | null=True,
22 | on_delete=django.db.models.deletion.SET_NULL,
23 | related_name="aliases",
24 | to="wagtailcore.page",
25 | ),
26 | ),
27 | ]
28 |
--------------------------------------------------------------------------------
/src/cast/views/wagtail_pagination.py:
--------------------------------------------------------------------------------
1 | from django.core.paginator import Page, Paginator
2 | from django.db.models import QuerySet
3 | from django.http import HttpRequest
4 |
5 | from ..appsettings import MENU_ITEM_PAGINATION
6 | from ..models import Audio, Transcript, Video
7 |
8 | DEFAULT_PAGE_KEY = "p"
9 |
10 | pagination_template = "wagtailadmin/shared/pagination_nav.html"
11 |
12 |
13 | def paginate(
14 | request: HttpRequest,
15 | items: QuerySet[Audio] | QuerySet[Video] | QuerySet[Transcript],
16 | page_key: str = DEFAULT_PAGE_KEY,
17 | per_page: int = MENU_ITEM_PAGINATION,
18 | ) -> tuple[Paginator, Page]:
19 | # if not items.query.order_by:
20 | # items = items.order_by("id")
21 | paginator: Paginator = Paginator(items, per_page)
22 | page = paginator.get_page(request.GET.get(page_key))
23 | return paginator, page
24 |
--------------------------------------------------------------------------------
/docs/releases/0.2.22.rst:
--------------------------------------------------------------------------------
1 | 0.2.22 (2023-09-18)
2 | -------------------
3 |
4 | Resolved a significant bug in theme selection. If you picked a theme
5 | stored in your session, the system would mistakenly apply a pre-selected
6 | theme for HTML fragments rendered through the JSON API. This was due
7 | to the real theme choice not being correctly passed from the JSON API
8 | to the Wagtail page, resulting in a completely dysfunctional Vue theme.
9 |
10 | - Bugfix theme selection #105
11 | - Fixed mypy issues by django-stubs update + one small fix #101
12 | - Improved documentation for theme selection #105
13 | - Got rid of ProxyRequest in favor of a simple HtmlField #105
14 | - Fixed searching for name instead of slug when filtering tags #100
15 | - Added a `has_selectable_themes` flag to the context of blog pages to make it easy to decide whether a theme selector can be showed #105
16 |
--------------------------------------------------------------------------------
/src/cast/api/viewmixins.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from django.forms import ModelForm
4 | from django.http import HttpRequest, HttpResponse
5 |
6 | logger = logging.getLogger(__name__)
7 |
8 |
9 | class FileUploadResponseMixin:
10 | @staticmethod
11 | def get_success_url() -> None:
12 | return None
13 |
14 | def form_valid(self, form: ModelForm) -> HttpResponse:
15 | model = form.save(commit=False)
16 | super().form_valid(form) # type: ignore
17 | return HttpResponse(f"{model.pk}", status=201)
18 |
19 |
20 | class AddRequestUserMixin:
21 | request: HttpRequest
22 | user_field_name = "user"
23 |
24 | def form_valid(self, form: ModelForm) -> bool:
25 | model = form.save(commit=False)
26 | setattr(model, self.user_field_name, self.request.user)
27 | return super().form_valid(form) # type: ignore
28 |
--------------------------------------------------------------------------------
/src/cast/comments/helper.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from crispy_forms.helper import FormHelper
4 | from django_comments import get_form_target
5 |
6 | from . import appsettings
7 |
8 |
9 | class CommentFormHelper(FormHelper):
10 | form_tag = False
11 | form_id = "comment-form-ID"
12 | form_class = f"js-comments-form {appsettings.FORM_CSS_CLASS}"
13 | label_class = appsettings.LABEL_CSS_CLASS
14 | field_class = appsettings.FIELD_CSS_CLASS
15 | render_unmentioned_fields = True
16 |
17 | @property
18 | def form_action(self) -> str:
19 | return get_form_target()
20 |
21 | def __init__(self, form=None):
22 | super().__init__(form=form)
23 | if form is not None:
24 | self.form_id = f"comment-form-{form.target_object.pk}"
25 | self.attrs = {"data-object-id": form.target_object.pk}
26 |
--------------------------------------------------------------------------------
/src/cast/views/meta.py:
--------------------------------------------------------------------------------
1 | from django.http import Http404, HttpRequest, HttpResponse
2 | from django.shortcuts import get_object_or_404, render
3 | from django.views.decorators.http import require_GET
4 |
5 | from ..models import Blog, Episode
6 |
7 |
8 | @require_GET
9 | def twitter_player(request: HttpRequest, blog_slug: str, episode_slug: str) -> HttpResponse:
10 | """
11 | This view is used to render the twitter card player. This is a
12 | podlove player consisting of just the play button. But it needs
13 | the episode data from the server.
14 | """
15 | blog = get_object_or_404(Blog, slug=blog_slug)
16 | episode = get_object_or_404(Episode, slug=episode_slug)
17 | if episode.blog != blog:
18 | raise Http404("Episode not found")
19 | context = {"episode": episode}
20 | return render(request, "cast/twitter/card_player.html", context=context)
21 |
--------------------------------------------------------------------------------
/src/cast/migrations/0047_alter_episode_podcast_audio.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.8 on 2023-04-29 15:27
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 | dependencies = [
9 | ("cast", "0046_alter_episode_podcast_audio"),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name="episode",
15 | name="podcast_audio",
16 | field=models.ForeignKey(
17 | blank=True,
18 | help_text="The audio file for this episode -if this is not set, the episode will not be included in the feed.",
19 | null=True,
20 | on_delete=django.db.models.deletion.SET_NULL,
21 | related_name="episodes",
22 | to="cast.audio",
23 | ),
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/src/cast/migrations/0056_add_cover_image_for_post.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.0.4 on 2024-05-08 09:52
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ("cast", "0055_alter_podcast_itunes_artwork"),
11 | ("wagtailimages", "0025_alter_image_file_alter_rendition_file"),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name="post",
17 | name="cover",
18 | field=models.ForeignKey(
19 | blank=True,
20 | help_text="An optional cover image.",
21 | null=True,
22 | on_delete=django.db.models.deletion.SET_NULL,
23 | related_name="+",
24 | to="wagtailimages.image",
25 | ),
26 | ),
27 | ]
28 |
--------------------------------------------------------------------------------
/src/cast/migrations/0046_alter_episode_podcast_audio.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.8 on 2023-04-09 06:12
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 | dependencies = [
9 | ("cast", "0045_alter_blog_template_base_dir_and_more"),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name="episode",
15 | name="podcast_audio",
16 | field=models.ForeignKey(
17 | blank=True,
18 | help_text="The audio file for this episode -if this is not set,the episode will not be included in the feed.",
19 | null=True,
20 | on_delete=django.db.models.deletion.SET_NULL,
21 | related_name="episodes",
22 | to="cast.audio",
23 | ),
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/docs/releases/0.2.35.rst:
--------------------------------------------------------------------------------
1 | 0.2.35 (2024-06-30)
2 | -------------------
3 |
4 | A lot of small improvements and bugfixes in this release.
5 |
6 | - Add defer to loading the javascript for the podlove web player
7 | - #56 removed old documentation
8 | - #136 Only use blog cover image as a fallback for episode cover images, not the iTunes artwork
9 | - #140 Workaround for the page_ptr ValueError in preview serving (ignore it)
10 | - #150 Make cover image alt text work by passing it through repositories
11 | - #152 Add a canonical link to the blog / podcast index page
12 | - #153 JavaScript dependency updates and new bundle
13 | - #154 Output all relative file paths + file contents to be able to pass it to the llm command
14 | - #155 Update theme endpoint breaks on integer json payloads
15 | - #156 Image IDs for galleries have to be integer
16 | - #157 Fix some chooser blocks breaking on integer values for get_prep_value
17 |
--------------------------------------------------------------------------------
/src/cast/templates/cast/plain/_filter_form.html:
--------------------------------------------------------------------------------
1 | {{ form.non_field_errors }}
2 |
27 |
--------------------------------------------------------------------------------
/tests/post_published_test.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from django.urls import reverse
3 |
4 |
5 | class TestPublished:
6 | pytestmark = pytest.mark.django_db
7 |
8 | def test_get_only_published_entries(self, client, unpublished_post):
9 | bp = unpublished_post
10 | feed_url = reverse("cast:latest_entries_feed", kwargs={"slug": bp.blog.slug})
11 |
12 | r = client.get(feed_url)
13 | assert r.status_code == 200
14 |
15 | content = r.content.decode("utf-8")
16 | assert "xml" in content
17 | assert bp.title not in content
18 |
19 | def test_get_post_detail_not_published_not_auth(self, client, unpublished_post):
20 | post = unpublished_post
21 | detail_url = post.get_url()
22 |
23 | r = client.get(detail_url)
24 | assert r.status_code == 404
25 |
26 | content = r.content.decode("utf-8")
27 | assert post.title not in content
28 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=.
11 | set BUILDDIR=_build
12 |
13 | %SPHINXBUILD% >NUL 2>NUL
14 | if errorlevel 9009 (
15 | echo.
16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
17 | echo.installed, then set the SPHINXBUILD environment variable to point
18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
19 | echo.may add the Sphinx directory to PATH.
20 | echo.
21 | echo.If you don't have Sphinx installed, grab it from
22 | echo.https://www.sphinx-doc.org/
23 | exit /b 1
24 | )
25 |
26 | if "%1" == "" goto help
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/tests/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import include, path
2 | from wagtail import urls as wagtail_urls
3 | from wagtail.admin import urls as wagtailadmin_urls
4 | from wagtail.documents import urls as wagtaildocs_urls
5 |
6 | from cast.views import defaults as default_views_cast
7 |
8 | handler404 = default_views_cast.page_not_found
9 | handler500 = default_views_cast.server_error
10 | handler400 = default_views_cast.bad_request
11 | handler403 = default_views_cast.permission_denied
12 |
13 |
14 | urlpatterns = [
15 | # rest framework docs/schema urls
16 | # re_path(r"^docs/", include_docs_urls(title="cast API service")),
17 | path("cast/", include("cast.urls", namespace="cast")),
18 | # comments
19 | path("posts/comments/", include("cast.comments.urls")),
20 | # wagtail
21 | path("cms/", include(wagtailadmin_urls)),
22 | path("documents/", include(wagtaildocs_urls)),
23 | path("", include(wagtail_urls)), # default is wagtail
24 | ]
25 |
--------------------------------------------------------------------------------
/example/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import os
4 | import sys
5 |
6 |
7 | def prepare_notebook_environment():
8 | from pathlib import Path
9 |
10 | os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
11 | os.chdir("notebooks")
12 | example_project_path = Path(__file__).resolve().parent
13 | os.environ["PYTHONPATH"] = str(example_project_path)
14 |
15 |
16 | if __name__ == "__main__":
17 | # handle DJANGO_SETTINGS_MODULE
18 | # os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_site.settings.dev")
19 | os.environ["DJANGO_SETTINGS_MODULE"] = "example_site.settings.dev"
20 |
21 | # Are we starting a notebook server? -> DJANGO_ALLOW_ASYNC_UNSAFE=true
22 | # chdir to notebooks and add example project to pythonpath
23 | if "--notebook" in set(sys.argv):
24 | prepare_notebook_environment()
25 |
26 | from django.core.management import execute_from_command_line
27 |
28 | execute_from_command_line(sys.argv)
29 |
--------------------------------------------------------------------------------
/src/cast/runner.py:
--------------------------------------------------------------------------------
1 | class PytestTestRunner:
2 | """Runs pytest to discover and run tests."""
3 |
4 | def __init__(self, verbosity: int = 1, failfast: bool = False, keepdb: bool = False, **_kwargs):
5 | self.verbosity = verbosity
6 | self.failfast = failfast
7 | self.keepdb = keepdb
8 |
9 | def run_tests(self, test_labels: list[str]) -> int:
10 | """Run pytest and return the exitcode.
11 |
12 | It translates some of Django's test command option to pytest's.
13 | """
14 | import pytest
15 |
16 | argv = []
17 | if self.verbosity == 0:
18 | argv.append("--quiet")
19 | if self.verbosity == 2:
20 | argv.append("--verbose")
21 | if self.verbosity == 3:
22 | argv.append("-vv")
23 | if self.failfast:
24 | argv.append("--exitfirst")
25 | if self.keepdb:
26 | argv.append("--reuse-db")
27 |
28 | argv.extend(test_labels)
29 | return pytest.main(argv)
30 |
--------------------------------------------------------------------------------
/src/cast/migrations/0005_auto_20201024_0613.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.2 on 2020-10-24 06:13
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ("wagtailcore", "0052_pagelogentry"),
11 | ("cast", "0004_homepage_alias_for_page"),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterField(
16 | model_name="homepage",
17 | name="alias_for_page",
18 | field=models.ForeignKey(
19 | blank=True,
20 | default=None,
21 | help_text="Make this page an alias for another page, redirecting to it with a non permanent redirect.",
22 | null=True,
23 | on_delete=django.db.models.deletion.SET_NULL,
24 | related_name="aliases",
25 | to="wagtailcore.page",
26 | verbose_name="Redirect to another page",
27 | ),
28 | ),
29 | ]
30 |
--------------------------------------------------------------------------------
/src/cast/migrations/0035_remove_new_prefix_podcast_fields.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.4 on 2023-02-04 14:00
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("cast", "0034_remove_old_podcast_fields_from_blog"),
10 | ]
11 |
12 | operations = [
13 | migrations.RenameField(
14 | model_name="podcast",
15 | old_name="new_explicit",
16 | new_name="explicit",
17 | ),
18 | migrations.RenameField(
19 | model_name="podcast",
20 | old_name="new_itunes_artwork",
21 | new_name="itunes_artwork",
22 | ),
23 | migrations.RenameField(
24 | model_name="podcast",
25 | old_name="new_itunes_categories",
26 | new_name="itunes_categories",
27 | ),
28 | migrations.RenameField(
29 | model_name="podcast",
30 | old_name="new_keywords",
31 | new_name="keywords",
32 | ),
33 | ]
34 |
--------------------------------------------------------------------------------
/src/cast/management/commands/media_replace.py:
--------------------------------------------------------------------------------
1 | from django.core.files.storage import FileSystemStorage
2 | from django.core.management.base import BaseCommand
3 |
4 | from .storage_backend import get_production_and_backup_storage
5 |
6 |
7 | class Command(BaseCommand):
8 | help = (
9 | "replace paths on production storage backend with local versions - useful for compressed videos for example"
10 | "(requires Django >= 4.2 and production and backup storage configured)"
11 | )
12 |
13 | def add_arguments(self, parser):
14 | parser.add_argument("paths", nargs="+", type=str)
15 |
16 | def handle(self, *args, **options):
17 | production, _ = get_production_and_backup_storage()
18 | fs_storage = FileSystemStorage()
19 | for path in options["paths"]:
20 | if fs_storage.exists(path):
21 | if production.exists(path):
22 | production.delete(path)
23 | with fs_storage.open(path, "rb") as in_f:
24 | production.save(path, in_f)
25 |
--------------------------------------------------------------------------------
/src/cast/static/fluent_comments/css/ajaxcomments.css:
--------------------------------------------------------------------------------
1 | .comment-waiting {
2 | line-height: 16px;
3 | }
4 |
5 | .comment-waiting img {
6 | vertical-align: middle;
7 | padding: 0 4px 0 10px;
8 | }
9 |
10 | .comment-added-message,
11 | #comment-thanks {
12 | padding-left: 10px;
13 | }
14 |
15 | .comment-moderated-flag {
16 | font-variant: small-caps;
17 | margin-left: 5px;
18 | }
19 |
20 | #div_id_honeypot {
21 | /* Hide the honeypot from django_comments by default */
22 | display: none;
23 | }
24 |
25 | /* ---- threaded comments ---- */
26 |
27 | ul.comment-list-wrapper {
28 | /* to avoid touching our own "ul" tags, our tags are explicitly decorated with a class selector */
29 | margin-left: 0;
30 | padding-left: 0;
31 | }
32 |
33 | ul.comment-list-wrapper ul.comment-list-wrapper {
34 | margin-left: 1em;
35 | padding-left: 0;
36 | }
37 |
38 | li.comment-wrapper {
39 | list-style: none;
40 | margin-left: 0;
41 | padding-left: 0;
42 | }
43 |
44 | .js-comments-form-orig-position .comment-cancel-reply-link {
45 | display: none;
46 | }
47 |
48 |
--------------------------------------------------------------------------------
/src/cast/comments/appsettings.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from django.conf import settings
4 |
5 | USE_THREADEDCOMMENTS: bool = "threadedcomments" in settings.INSTALLED_APPS
6 |
7 | # Prefer CAST_* settings, but allow existing deployments to keep their historic
8 | # FLUENT_* names while migrating.
9 | EXCLUDE_FIELDS: tuple[str, ...] = tuple(
10 | getattr(settings, "CAST_COMMENTS_EXCLUDE_FIELDS", getattr(settings, "FLUENT_COMMENTS_EXCLUDE_FIELDS", ())) or ()
11 | )
12 |
13 | DEFAULT_MODERATOR: str = getattr(
14 | settings,
15 | "CAST_COMMENTS_DEFAULT_MODERATOR",
16 | getattr(settings, "FLUENT_COMMENTS_DEFAULT_MODERATOR", "cast.moderation.Moderator"),
17 | )
18 |
19 | CRISPY_TEMPLATE_PACK: str = getattr(settings, "CRISPY_TEMPLATE_PACK", "bootstrap")
20 |
21 | FORM_CSS_CLASS: str = getattr(settings, "CAST_COMMENTS_FORM_CSS_CLASS", "comments-form form-horizontal")
22 | LABEL_CSS_CLASS: str = getattr(settings, "CAST_COMMENTS_LABEL_CSS_CLASS", "col-sm-2")
23 | FIELD_CSS_CLASS: str = getattr(settings, "CAST_COMMENTS_FIELD_CSS_CLASS", "col-sm-10")
24 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # For the full list of built-in configuration values, see the documentation:
4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
5 |
6 | # -- Project information -----------------------------------------------------
7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
8 |
9 | project = "Django Cast"
10 | copyright = "2025, Jochen Wersdörfer"
11 | author = "Jochen Wersdörfer"
12 | release = "0.2.50"
13 |
14 | # -- General configuration ---------------------------------------------------
15 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
16 |
17 | extensions = []
18 |
19 | templates_path = ["_templates"]
20 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
21 |
22 |
23 | # -- Options for HTML output -------------------------------------------------
24 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
25 |
26 | html_theme = "furo"
27 | html_static_path = ["_static"]
28 |
--------------------------------------------------------------------------------
/src/cast/migrations/0058_add_cover_image_to_blog.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.0.4 on 2024-06-12 16:10
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ("cast", "0057_rename_cover_image_and_add_alt_text"),
11 | ("wagtailimages", "0025_alter_image_file_alter_rendition_file"),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name="blog",
17 | name="cover_alt_text",
18 | field=models.CharField(blank=True, default="", max_length=255),
19 | ),
20 | migrations.AddField(
21 | model_name="blog",
22 | name="cover_image",
23 | field=models.ForeignKey(
24 | blank=True,
25 | help_text="An optional cover image.",
26 | null=True,
27 | on_delete=django.db.models.deletion.SET_NULL,
28 | related_name="+",
29 | to="wagtailimages.image",
30 | ),
31 | ),
32 | ]
33 |
--------------------------------------------------------------------------------
/src/cast/comments/utils.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from django.contrib.contenttypes.models import ContentType
4 |
5 | from . import appsettings
6 |
7 |
8 | def get_comment_template_name(comment):
9 | ctype = ContentType.objects.get_for_id(comment.content_type_id)
10 | return [
11 | f"comments/{ctype.app_label}/{ctype.model}/comment.html",
12 | f"comments/{ctype.app_label}/comment.html",
13 | "comments/comment.html",
14 | ]
15 |
16 |
17 | def get_comment_context_data(comment, action: str | None = None):
18 | return {
19 | "comment": comment,
20 | "action": action,
21 | "preview": (action == "preview"),
22 | "USE_THREADEDCOMMENTS": appsettings.USE_THREADEDCOMMENTS,
23 | }
24 |
25 |
26 | def comments_are_open(content_object) -> bool:
27 | value = getattr(content_object, "comments_are_enabled", None)
28 | if callable(value):
29 | return bool(value())
30 | if value is not None:
31 | return bool(value)
32 | return True
33 |
34 |
35 | def comments_are_moderated(content_object) -> bool:
36 | return False
37 |
--------------------------------------------------------------------------------
/src/cast/migrations/0007_alter_post_body.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.5 on 2021-07-01 08:34
2 |
3 | import wagtail.blocks
4 | import wagtail.fields
5 | import wagtail.embeds.blocks
6 | import wagtail.images.blocks
7 | from django.db import migrations
8 |
9 | import cast.blocks
10 |
11 |
12 | class Migration(migrations.Migration):
13 |
14 | dependencies = [
15 | ("cast", "0006_auto_20210628_1628"),
16 | ]
17 |
18 | operations = [
19 | migrations.AlterField(
20 | model_name="post",
21 | name="body",
22 | field=wagtail.fields.StreamField(
23 | [
24 | ("heading", wagtail.blocks.CharBlock(form_classname="full title")),
25 | ("paragraph", wagtail.blocks.RichTextBlock()),
26 | ("image", wagtail.images.blocks.ImageChooserBlock(template="cast/image/image.html")),
27 | ("gallery", cast.blocks.GalleryBlock(wagtail.images.blocks.ImageChooserBlock())),
28 | ("embed", wagtail.embeds.blocks.EmbedBlock()),
29 | ]
30 | ),
31 | ),
32 | ]
33 |
--------------------------------------------------------------------------------
/src/cast/comments/models.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from django.utils.translation import gettext_lazy as _
4 | from django_comments.managers import CommentManager
5 |
6 | from . import appsettings
7 |
8 | from django_comments.models import Comment as DjangoComment
9 |
10 | try:
11 | from threadedcomments.models import ThreadedComment as ThreadedCommentModel
12 | except ImportError: # pragma: no cover
13 | ThreadedCommentModel = None
14 |
15 |
16 | def get_base_comment_model() -> type[DjangoComment]:
17 | if appsettings.USE_THREADEDCOMMENTS and ThreadedCommentModel is not None:
18 | return ThreadedCommentModel
19 | return DjangoComment
20 |
21 |
22 | BaseComment = get_base_comment_model()
23 |
24 |
25 | class CastCommentManager(CommentManager):
26 | def get_queryset(self):
27 | return super().get_queryset().select_related("user")
28 |
29 |
30 | class CastComment(BaseComment):
31 | objects = CastCommentManager()
32 |
33 | class Meta:
34 | verbose_name = _("Comment")
35 | verbose_name_plural = _("Comments")
36 | proxy = True
37 | managed = False
38 |
--------------------------------------------------------------------------------
/src/cast/models/__init__.py:
--------------------------------------------------------------------------------
1 | from .audio import Audio, ChapterMark, sync_chapter_marks
2 | from .file import File
3 | from .gallery import Gallery, get_or_create_gallery
4 | from .index_pages import Blog, Podcast
5 | from .itunes import ItunesArtWork
6 | from .moderation import SpamFilter
7 | from .pages import Episode, HomePage, Post, sync_media_ids
8 | from .snippets import PostCategory
9 | from .theme import (
10 | TemplateBaseDirectory,
11 | get_template_base_dir,
12 | get_template_base_dir_choices,
13 | )
14 | from .transcript import Transcript
15 | from .video import Video, get_video_dimensions
16 |
17 | __all__ = [
18 | "Audio",
19 | "ChapterMark",
20 | "sync_chapter_marks",
21 | "Blog",
22 | "File",
23 | "Gallery",
24 | "get_or_create_gallery",
25 | "get_template_base_dir",
26 | "get_template_base_dir_choices",
27 | "HomePage",
28 | "ItunesArtWork",
29 | "Post",
30 | "PostCategory",
31 | "Episode",
32 | "sync_media_ids",
33 | "SpamFilter",
34 | "Transcript",
35 | "Video",
36 | "get_video_dimensions",
37 | "Podcast",
38 | "TemplateBaseDirectory",
39 | ]
40 |
--------------------------------------------------------------------------------
/docs/releases/0.2.47.rst:
--------------------------------------------------------------------------------
1 | 0.2.47 (2025-07-12)
2 | -------------------
3 |
4 | New Features:
5 |
6 | - #190 Add django-cast-quickstart CLI command for quick project setup
7 | - #190 Add CAST_APPS and CAST_MIDDLEWARE constants for simplified installation
8 |
9 | Documentation:
10 |
11 | - #190 Major documentation restructure and improvements
12 | - #190 Add comprehensive architecture documentation
13 | - #190 Add comprehensive API documentation
14 | - #190 Enhance models documentation with comprehensive coverage
15 | - #190 Add comprehensive StreamField documentation
16 | - #190 Add comprehensive media handling documentation
17 | - Fix Furo theme issues and documentation build warnings
18 | - Improve documentation structure for better organization
19 | - Consolidate development documentation
20 |
21 | Build and Infrastructure:
22 |
23 | - Exclude Jupyter notebooks and migrations from ruff formatting
24 | - Fix E402 linting errors by moving imports to top of files
25 | - Minor code quality improvements with ruff
26 | - Update JavaScript dependencies (Vite 7.0.2 → 7.0.4)
27 | - Rebuild production JavaScript bundles
28 | - Update pre-commit hooks (Ruff 0.8.0 → 0.12.3)
29 |
--------------------------------------------------------------------------------
/src/cast/templates/base.html:
--------------------------------------------------------------------------------
1 | {% load static i18n %}
2 |
3 |
4 | {% block meta %}
5 |
6 |
7 |
8 |
9 |
10 | {% endblock %}
11 |
12 | {% block css %}
13 |
14 | {% endblock %}
15 |
16 |
17 |
18 |
19 |
20 | {% block content %}
21 |
Use this document as a way to quick start any new project.
22 | {% endblock content %}
23 |
24 |
25 |
26 | {% block modal %}{% endblock modal %}
27 |
28 |
30 |
31 | {% block javascript %}
32 | {% endblock javascript %}
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/cast/context_processors.py:
--------------------------------------------------------------------------------
1 | from django.db import IntegrityError
2 | from django.http import HttpRequest
3 |
4 | from .models import TemplateBaseDirectory
5 |
6 | DEFAULT_TEMPLATE_BASE_DIR = "does_not_exist"
7 |
8 |
9 | def site_template_base_dir(request: HttpRequest) -> dict[str, str]:
10 | """
11 | Add the name of the template base directory to the context.
12 | Add the complete base template path to the context for convenience.
13 | """
14 | if hasattr(request, "cast_site_template_base_dir"):
15 | site_template_base_dir_name = request.cast_site_template_base_dir
16 | else:
17 | try:
18 | site_template_base_dir_name = TemplateBaseDirectory.for_request(request).name
19 | except (TemplateBaseDirectory.DoesNotExist, IntegrityError):
20 | # If the site template base directory does not exist, use the default
21 | # need to catch IntegrityError because of Wagtail5 support
22 | site_template_base_dir_name = DEFAULT_TEMPLATE_BASE_DIR
23 | return {
24 | "cast_site_template_base_dir": site_template_base_dir_name,
25 | "cast_base_template": f"cast/{site_template_base_dir_name}/base.html",
26 | }
27 |
--------------------------------------------------------------------------------
/src/cast/templates/cast/bootstrap4/post_body.html:
--------------------------------------------------------------------------------
1 | {% load wagtailcore_tags %}
2 | {% load i18n %}
3 |
4 |
5 |
6 |
7 | {{ page.title }}
8 | {% if render_for_feed and comments_are_enabled %}
9 | {% translate "(click here to comment)" %}
10 | {% endif %}
11 |
30 |
31 | Link to RSS feed
32 |
33 |
34 |
35 | {% endblock header %}
36 |
37 | {% block navigation %}
38 |
45 | {% endblock navigation %}
46 |
47 | {% block main %}
48 |
49 | {% include "./pagination.html" %}
50 | {% for post in posts %}
51 | {% include "./post_body.html" with render_detail=False page=post %}
52 | {% endfor %}
53 |
54 | {% endblock main %}
55 |
56 | {% block footer %}
57 |
60 | {% endblock footer %}
61 |
--------------------------------------------------------------------------------
/src/cast/comments/receivers.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from django.core.exceptions import ImproperlyConfigured
4 | from django.dispatch import receiver
5 | from django.utils.functional import SimpleLazyObject
6 | from django.utils.module_loading import import_string
7 | from django_comments import signals
8 |
9 | from . import appsettings
10 |
11 |
12 | class NullModerator:
13 | def __init__(self, model) -> None:
14 | self.model = model
15 |
16 | def allow(self, comment, content_object, request) -> bool:
17 | return True
18 |
19 | def moderate(self, comment, content_object, request) -> bool:
20 | return False
21 |
22 |
23 | def load_default_moderator():
24 | value = str(appsettings.DEFAULT_MODERATOR).strip()
25 | if value.lower() in {"none", "null"}:
26 | return NullModerator(None)
27 | if "." in value:
28 | return import_string(value)(None)
29 | if value.lower() in {"default", ""}:
30 | return NullModerator(None)
31 | raise ImproperlyConfigured(
32 | "Bad CAST_COMMENTS_DEFAULT_MODERATOR/FLUENT_COMMENTS_DEFAULT_MODERATOR value. Provide 'none' or a dotted path."
33 | )
34 |
35 |
36 | default_moderator = SimpleLazyObject(load_default_moderator)
37 |
38 |
39 | @receiver(signals.comment_will_be_posted)
40 | def on_comment_will_be_posted(sender, comment, request, **kwargs):
41 | content_object = comment.content_object
42 | if not default_moderator.allow(comment, content_object, request):
43 | return False
44 | default_moderator.moderate(comment, content_object, request)
45 | return None
46 |
--------------------------------------------------------------------------------
/src/cast/migrations/0045_alter_blog_template_base_dir_and_more.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.7 on 2023-03-18 07:47
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("cast", "0044_alter_blog_template_base_dir_and_more"),
9 | ]
10 |
11 | operations = [
12 | migrations.AlterField(
13 | model_name="blog",
14 | name="template_base_dir",
15 | field=models.CharField(
16 | blank=True,
17 | choices=[("bootstrap4", "Bootstrap 4"), ("plain", "Just HTML")],
18 | default=None,
19 | help_text="The theme to use for this blog implemented as a template base directory. If not set, the template base directory will be determined by a site setting.",
20 | max_length=128,
21 | null=True,
22 | ),
23 | ),
24 | migrations.AlterField(
25 | model_name="templatebasedirectory",
26 | name="name",
27 | field=models.CharField(
28 | choices=[("bootstrap4", "Bootstrap 4"), ("plain", "Just HTML")],
29 | default="bootstrap4",
30 | help_text="The theme to use for this site implemented as a template base directory. It's possible to overwrite this setting for each blog.If you want to use a custom theme, you have to create a new directory in your template directory named cast// and put all required templates in there.",
31 | max_length=128,
32 | ),
33 | ),
34 | ]
35 |
--------------------------------------------------------------------------------
/docs/content/organization.rst:
--------------------------------------------------------------------------------
1 | .. _content_organization:
2 |
3 | ********************
4 | Content Organization
5 | ********************
6 |
7 | Django Cast provides flexible ways to organize and categorize your content through tags and categories. This helps readers discover related content and improves site navigation.
8 |
9 | *****************
10 | Categories / Tags
11 | *****************
12 |
13 | This is a beta feature. It is not yet fully implemented. Since I don't
14 | know yet if I will go with tags or categories, I added both and wait
15 | which one sticks 😄.
16 |
17 | Categories
18 | ==========
19 |
20 | Categories are one way to group posts. They come with their own snippet
21 | model so you can add them via the admin interface by clicking on one
22 | of the categories. A blog post can have multiple categories and a category
23 | can have multiple blog posts. If you want to add a new category, you have
24 | to add it using the wagtail admin interface.
25 |
26 | Categories might be the right thing if you do not have too many of
27 | them and they rarely change.
28 |
29 |
30 | Tags
31 | ====
32 |
33 | Tags are another way to group posts. They come with their own link to
34 | the `taggit` tag model. You can add tags to a blog post by using the
35 | standard wagtail `tag` interface. A blog post can have multiple tags
36 | and a tag can have multiple blog posts. If you want to add a new tag,
37 | there's a text field with auto completion in the wagtail admin interface.
38 |
39 | Tags might be the right thing if you have a lot of them and they change
40 | often and you don't mind having to type them in the admin interface.
41 |
--------------------------------------------------------------------------------
/docs/releases/index.rst:
--------------------------------------------------------------------------------
1 | ********
2 | Releases
3 | ********
4 |
5 | Versions
6 | ========
7 |
8 | .. toctree::
9 | :maxdepth: 1
10 |
11 | 0.2.50
12 | 0.2.49
13 | 0.2.48
14 | 0.2.47
15 | 0.2.46
16 | 0.2.45
17 | 0.2.44
18 | 0.2.43
19 | 0.2.42
20 | 0.2.41
21 | 0.2.40
22 | 0.2.39
23 | 0.2.38
24 | 0.2.37
25 | 0.2.36
26 | 0.2.35
27 | 0.2.34
28 | 0.2.33
29 | 0.2.32
30 | 0.2.31
31 | 0.2.30
32 | 0.2.29
33 | 0.2.28
34 | 0.2.27
35 | 0.2.26
36 | 0.2.25
37 | 0.2.24
38 | 0.2.23
39 | 0.2.22
40 | 0.2.21
41 | 0.2.20
42 | 0.2.19
43 | 0.2.18
44 | 0.2.17
45 | 0.2.16
46 | 0.2.15
47 | 0.2.14
48 | 0.2.13
49 | 0.2.12
50 | 0.2.11
51 | 0.2.10
52 | 0.2.9
53 | 0.2.8
54 | 0.2.7
55 | 0.2.6
56 | 0.2.5
57 | 0.2.4
58 | 0.2.3
59 | 0.2.2
60 | 0.2.1
61 | 0.2.0
62 | 0.1/0.1.35
63 | 0.1/0.1.34
64 | 0.1/0.1.33
65 | 0.1/0.1.32
66 | 0.1/0.1.31
67 | 0.1/0.1.30
68 | 0.1/0.1.29
69 | 0.1/0.1.28
70 | 0.1/0.1.27
71 | 0.1/0.1.26
72 | 0.1/0.1.25
73 | 0.1/0.1.24
74 | 0.1/0.1.23
75 | 0.1/0.1.22
76 | 0.1/0.1.21
77 | 0.1/0.1.20
78 | 0.1/0.1.19
79 | 0.1/0.1.18
80 | 0.1/0.1.17
81 | 0.1/0.1.16
82 | 0.1/0.1.15
83 | 0.1/0.1.14
84 | 0.1/0.1.13
85 | 0.1/0.1.12
86 | 0.1/0.1.11
87 | 0.1/0.1.10
88 | 0.1/0.1.9
89 | 0.1/0.1.8
90 | 0.1/0.1.7
91 | 0.1/0.1.6
92 | 0.1/0.1.5
93 | 0.1/0.1.4
94 | 0.1/0.1.3
95 | 0.1/0.1.2
96 | 0.1/0.1.1
97 | 0.1/0.1.0
98 |
99 | Versioning Policy
100 | =================
101 |
102 | To be defined 😅.
103 |
--------------------------------------------------------------------------------