├── .editorconfig ├── .github ├── ISSUE_TEMPLATE.md └── workflows │ └── workflow.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── .travis.yml ├── AUTHORS.rst ├── CONTRIBUTING.rst ├── LICENSE ├── Procfile ├── README.md ├── bootstrap.md ├── cast ├── __init__.py ├── admin.py ├── admin_urls │ ├── audio.py │ ├── transcript.py │ └── video.py ├── api │ ├── __init__.py │ ├── serializers.py │ ├── urls.py │ ├── viewmixins.py │ └── views.py ├── apps.py ├── appsettings.py ├── blocks.py ├── cast_and_wagtail_urls.py ├── context_processors.py ├── devdata.py ├── feeds.py ├── filters.py ├── forms.py ├── locale │ └── de │ │ └── LC_MESSAGES │ │ └── django.po ├── management │ └── commands │ │ ├── __init__.py │ │ ├── media_backup.py │ │ ├── media_replace.py │ │ ├── media_restore.py │ │ ├── media_sizes.py │ │ ├── media_stale.py │ │ ├── recalc_video_posters.py │ │ ├── storage_backend.py │ │ └── sync_renditions.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_remove_blog_description.py │ ├── 0003_remove_post_parent_blog.py │ ├── 0004_homepage_alias_for_page.py │ ├── 0005_auto_20201024_0613.py │ ├── 0006_auto_20210628_1628.py │ ├── 0007_alter_post_body.py │ ├── 0008_auto_20210712_0919.py │ ├── 0009_alter_post_body.py │ ├── 0010_rename_intro_blog_description.py │ ├── 0011_alter_post_body.py │ ├── 0012_alter_post_images.py │ ├── 0013_alter_gallery_images.py │ ├── 0014_remove_gallery_user.py │ ├── 0015_delete_blogindexpage.py │ ├── 0016_auto_20210830_0422.py │ ├── 0017_alter_post_body.py │ ├── 0018_alter_chaptermark_start.py │ ├── 0019_alter_chaptermark_start.py │ ├── 0020_auto_20210926_1556.py │ ├── 0021_spamfilter.py │ ├── 0022_alter_spamfilter_model.py │ ├── 0023_alter_spamfilter_model.py │ ├── 0024_alter_homepage_body_alter_post_body.py │ ├── 0025_add_performance_indicators.py │ ├── 0026_delete_request_alter_post_body.py │ ├── 0027_episode.py │ ├── 0028_rename_and_drop_itune_fields.py │ ├── 0029_add_metadata_field_to_audio.py │ ├── 0030_remove_pub_date_is_handled_by_wagtail.py │ ├── 0031_remove_timestampedmodel_because_wagtail.py │ ├── 0032_remove_timestampedmodel_because_wagtail.py │ ├── 0033_add_new_podcast_model.py │ ├── 0034_remove_old_podcast_fields_from_blog.py │ ├── 0035_remove_new_prefix_podcast_fields.py │ ├── 0036_alter_blog_author.py │ ├── 0037_alter_episode_block_alter_episode_explicit_and_more.py │ ├── 0038_alter_episode_keywords.py │ ├── 0039_blog_noindex.py │ ├── 0040_alter_blog_noindex.py │ ├── 0041_templatebasedirectory.py │ ├── 0042_blog_template_base_dir.py │ ├── 0043_alter_blog_template_base_dir.py │ ├── 0044_alter_blog_template_base_dir_and_more.py │ ├── 0045_alter_blog_template_base_dir_and_more.py │ ├── 0046_alter_episode_podcast_audio.py │ ├── 0047_alter_episode_podcast_audio.py │ ├── 0048_added_visible_date_index_for_wagtail_api.py │ ├── 0049_added_category_snippets.py │ ├── 0050_add_tags_for_posts.py │ ├── 0051_use_own_image_chooser_block.py │ ├── 0052_alter_blog_template_base_dir_alter_post_body_and_more.py │ ├── 0053_rename_default_layout.py │ ├── 0054_alter_blog_template_base_dir_and_more.py │ ├── 0055_alter_podcast_itunes_artwork.py │ ├── 0056_add_cover_image_for_post.py │ ├── 0057_rename_cover_image_and_add_alt_text.py │ ├── 0058_add_cover_image_to_blog.py │ ├── 0059_add_subtitle.py │ ├── 0060_transcript.py │ ├── 0061_alter_audio_options.py │ ├── 0062_alter_transcript_options.py │ └── __init__.py ├── models │ ├── __init__.py │ ├── audio.py │ ├── file.py │ ├── gallery.py │ ├── image_renditions.py │ ├── index_pages.py │ ├── itunes.py │ ├── moderation.py │ ├── pages.py │ ├── repository.py │ ├── snippets.py │ ├── theme.py │ ├── transcript.py │ └── video.py ├── moderation.py ├── renditions.py ├── runner.py ├── settings.py ├── static │ └── cast │ │ ├── css │ │ ├── bootstrap4 │ │ │ ├── bootstrap.min.css │ │ │ └── cast.css │ │ ├── my_div.css │ │ └── plain │ │ │ └── cast.css │ │ ├── img │ │ ├── Audio-icon.svg │ │ ├── Feed-icon.svg │ │ └── Video-icon.svg │ │ ├── js │ │ ├── bootstrap4 │ │ │ ├── bootstrap.min.js │ │ │ └── bootstrap.min.js.map │ │ ├── htmx.min.js │ │ ├── jquery │ │ │ └── jquery-3.7.1.min.js │ │ ├── wagtail │ │ │ ├── audio-chooser-modal.js │ │ │ ├── audio-chooser-telepath.js │ │ │ ├── audio-chooser.js │ │ │ ├── video-chooser-modal.js │ │ │ ├── video-chooser-telepath.js │ │ │ └── video-chooser.js │ │ └── web-player │ │ │ └── embed.5.js │ │ └── vite │ │ ├── main-CCUgFhRa.js │ │ ├── manifest.json │ │ └── podlovePlayer-BhApzi9J.js ├── templates │ ├── base.html │ ├── cast │ │ ├── audio │ │ │ ├── add.html │ │ │ ├── audio.html │ │ │ ├── chooser.html │ │ │ ├── chooser_chooser.html │ │ │ ├── chooser_results.html │ │ │ ├── confirm_delete.html │ │ │ ├── edit.html │ │ │ ├── index.html │ │ │ ├── list.html │ │ │ └── results.html │ │ ├── base.html │ │ ├── bootstrap4 │ │ │ ├── 400.html │ │ │ ├── 403.html │ │ │ ├── 403_csrf.html │ │ │ ├── 404.html │ │ │ ├── 500.html │ │ │ ├── _list_of_posts_and_paging_controls.html │ │ │ ├── base.html │ │ │ ├── blog_list_of_posts.html │ │ │ ├── episode.html │ │ │ ├── gallery.html │ │ │ ├── gallery_htmx.html │ │ │ ├── gallery_modal.html │ │ │ ├── pagination.html │ │ │ ├── post.html │ │ │ ├── post_body.html │ │ │ ├── select_theme.html │ │ │ └── transcript.html │ │ ├── cast_base.html │ │ ├── gallery.html │ │ ├── home_page.html │ │ ├── image │ │ │ └── image.html │ │ ├── image_form.html │ │ ├── plain │ │ │ ├── 400.html │ │ │ ├── 403.html │ │ │ ├── 403_csrf.html │ │ │ ├── 404.html │ │ │ ├── 500.html │ │ │ ├── _filter_form.html │ │ │ ├── _list_of_posts_and_paging_controls.html │ │ │ ├── base.html │ │ │ ├── blog_list_of_posts.html │ │ │ ├── episode.html │ │ │ ├── gallery_modal.html │ │ │ ├── pagination.html │ │ │ ├── post.html │ │ │ ├── post_body.html │ │ │ └── select_theme.html │ │ ├── transcript │ │ │ ├── add.html │ │ │ ├── chooser.html │ │ │ ├── chooser_chooser.html │ │ │ ├── chooser_results.html │ │ │ ├── confirm_delete.html │ │ │ ├── edit.html │ │ │ ├── index.html │ │ │ ├── list.html │ │ │ └── results.html │ │ ├── twitter │ │ │ └── card_player.html │ │ ├── video │ │ │ ├── add.html │ │ │ ├── chooser.html │ │ │ ├── chooser_chooser.html │ │ │ ├── chooser_results.html │ │ │ ├── confirm_delete.html │ │ │ ├── edit.html │ │ │ ├── index.html │ │ │ ├── list.html │ │ │ ├── results.html │ │ │ └── video.html │ │ ├── video_form.html │ │ └── wagtail │ │ │ ├── _file_field.html │ │ │ ├── _file_field_as_li.html │ │ │ ├── _thumbnail_field.html │ │ │ └── _thumbnail_field_as_li.html │ └── comments │ │ └── comment.html ├── urls.py ├── utils.py ├── views │ ├── __init__.py │ ├── audio.py │ ├── defaults.py │ ├── gallery.py │ ├── htmx_helpers.py │ ├── meta.py │ ├── theme.py │ ├── transcript.py │ ├── video.py │ └── wagtail_pagination.py ├── wagtail_hooks.py └── widgets.py ├── command_lines.txt ├── commands.py ├── django-cast-example.ps1 ├── docs ├── Makefile ├── audio.rst ├── authors.rst ├── backup.rst ├── blog.rst ├── comments.rst ├── conf.py ├── context-processors.rst ├── contributing.rst ├── django-admin.rst ├── episode.rst ├── features.rst ├── frontend.rst ├── gallery.rst ├── howto │ ├── first-cast.rst │ ├── index.rst │ └── integrate-cast.rst ├── image.rst ├── images │ ├── blog_template_base_dir_setting.png │ ├── cache_file_sizes_admin_action.png │ ├── spam_filter_performance.png │ ├── template_base_dir_setting.png │ └── twitter_card.png ├── index.rst ├── installation.rst ├── make.bat ├── management-commands.rst ├── models.rst ├── podcast.rst ├── post.rst ├── release.rst ├── releases │ ├── 0.1 │ │ ├── 0.1.0.rst │ │ ├── 0.1.1.rst │ │ ├── 0.1.10.rst │ │ ├── 0.1.11.rst │ │ ├── 0.1.12.rst │ │ ├── 0.1.13.rst │ │ ├── 0.1.14.rst │ │ ├── 0.1.15.rst │ │ ├── 0.1.16.rst │ │ ├── 0.1.17.rst │ │ ├── 0.1.18.rst │ │ ├── 0.1.19.rst │ │ ├── 0.1.2.rst │ │ ├── 0.1.20.rst │ │ ├── 0.1.21.rst │ │ ├── 0.1.22.rst │ │ ├── 0.1.23.rst │ │ ├── 0.1.24.rst │ │ ├── 0.1.25.rst │ │ ├── 0.1.26.rst │ │ ├── 0.1.27.rst │ │ ├── 0.1.28.rst │ │ ├── 0.1.29.rst │ │ ├── 0.1.3.rst │ │ ├── 0.1.30.rst │ │ ├── 0.1.31.rst │ │ ├── 0.1.32.rst │ │ ├── 0.1.33.rst │ │ ├── 0.1.34.rst │ │ ├── 0.1.35.rst │ │ ├── 0.1.4.rst │ │ ├── 0.1.5.rst │ │ ├── 0.1.6.rst │ │ ├── 0.1.7.rst │ │ ├── 0.1.8.rst │ │ └── 0.1.9.rst │ ├── 0.2.0.rst │ ├── 0.2.1.rst │ ├── 0.2.10.rst │ ├── 0.2.11.rst │ ├── 0.2.12.rst │ ├── 0.2.13.rst │ ├── 0.2.14.rst │ ├── 0.2.15.rst │ ├── 0.2.16.rst │ ├── 0.2.17.rst │ ├── 0.2.18.rst │ ├── 0.2.19.rst │ ├── 0.2.2.rst │ ├── 0.2.20.rst │ ├── 0.2.21.rst │ ├── 0.2.22.rst │ ├── 0.2.23.rst │ ├── 0.2.24.rst │ ├── 0.2.25.rst │ ├── 0.2.26.rst │ ├── 0.2.27.rst │ ├── 0.2.28.rst │ ├── 0.2.29.rst │ ├── 0.2.3.rst │ ├── 0.2.30.rst │ ├── 0.2.31.rst │ ├── 0.2.32.rst │ ├── 0.2.33.rst │ ├── 0.2.34.rst │ ├── 0.2.35.rst │ ├── 0.2.36.rst │ ├── 0.2.37.rst │ ├── 0.2.38.rst │ ├── 0.2.39.rst │ ├── 0.2.4.rst │ ├── 0.2.40.rst │ ├── 0.2.41.rst │ ├── 0.2.42.rst │ ├── 0.2.43.rst │ ├── 0.2.44.rst │ ├── 0.2.45.rst │ ├── 0.2.5.rst │ ├── 0.2.6.rst │ ├── 0.2.7.rst │ ├── 0.2.8.rst │ ├── 0.2.9.rst │ └── index.rst ├── requirements.txt ├── responsive-images.rst ├── settings.rst ├── social-media.rst ├── tags.rst ├── tests.rst ├── themes.rst ├── transcript.rst └── video.rst ├── example ├── example_site │ ├── __init__.py │ ├── settings │ │ ├── __init__.py │ │ ├── base.py │ │ ├── dev.py │ │ └── production.py │ ├── templates │ │ ├── 404.html │ │ ├── 500.html │ │ ├── base.html │ │ └── pages │ │ │ └── about.html │ ├── urls.py │ └── wsgi.py ├── manage.py └── staticfiles │ ├── css │ └── project.css │ └── js │ └── project.js ├── javascript ├── .gitignore ├── jest.config.js ├── package-lock.json ├── package.json ├── src │ ├── audio │ │ └── podlove-player.ts │ ├── gallery │ │ └── image-gallery-bs4.ts │ └── tests │ │ ├── image-gallery-bs4.test.ts │ │ └── podlove-player.test.ts ├── tsconfig.json └── vite.config.ts ├── manage.py ├── notebooks ├── api │ └── get_pages_from_production.ipynb ├── audio │ ├── audio_handling.ipynb │ ├── chaptermarks_form.ipynb │ ├── file_sizes_debug.ipynb │ └── read_chaptermarks_from_audio_file.ipynb ├── debug │ ├── debug.ipynb │ ├── debug_add_new_post.ipynb │ ├── debug_blog_post_list.ipynb │ ├── debug_comments_templates.ipynb │ ├── django_upgrade_debug.ipynb │ ├── pagination.ipynb │ ├── post_get_absolute_url.ipynb │ ├── python_3.10_debug.ipynb │ ├── remove_obsolete_symlinks.ipynb │ ├── render_ajax.ipynb │ ├── sync_media_ids.ipynb │ ├── test_file_handling.ipynb │ └── video_exploration_foo.ipynb ├── feed │ ├── feed_structure.ipynb │ └── remove_timestamped_model.ipynb ├── image │ ├── from_pillow_to_willow.ipynb │ └── gallery_tests.ipynb ├── logs │ └── broken_access_log.ipynb ├── moderation │ ├── 2023-01-04_improve_spamfilter.ipynb │ ├── comments.json │ ├── comments_api_endpoint_debug.ipynb │ ├── example_site │ ├── get_comments_from_production.ipynb │ ├── moderation_debug.ipynb │ ├── naive_bayes.ipynb │ ├── naive_bayes_class.ipynb │ ├── naive_bayes_from_scratch.ipynb │ ├── sms_dataset_to_messages.ipynb │ ├── spamfilter.ipynb │ └── spamfilter_on_comments_from_db.ipynb ├── offtopic │ └── dice.ipynb ├── search │ └── faceted_navigation_debug.ipynb ├── twitter │ ├── player_card.ipynb │ └── twitter_player_card.ipynb └── wagtail │ ├── add_child_debug.ipynb │ ├── api.ipynb │ ├── code_block.ipynb │ ├── create_content_wagtail.ipynb │ ├── missing_audio.ipynb │ ├── split_post_into_post_and_episode.ipynb │ ├── template_settings.ipynb │ ├── test_wagtail_views.ipynb │ ├── wagtail_get_descendants_of_page_statements.ipynb │ ├── wagtail_render_post.ipynb │ └── wagtail_video_views.ipynb ├── pyproject.toml ├── runtests.py ├── setup.cfg ├── tests ├── __init__.py ├── admin_test.py ├── api_test.py ├── apps_test.py ├── audio_models_test.py ├── audio_views_test.py ├── blocks_test.py ├── blog_index_test.py ├── cast │ └── static │ │ └── .gitkeep ├── comments_test.py ├── conftest.py ├── convenience_test.py ├── episode_detail_test.py ├── error_handler_views_test.py ├── factories.py ├── feed_test.py ├── filter_test.py ├── fixtures │ ├── access.log │ ├── test.m4a │ ├── test.mp4 │ └── test_video.mp4 ├── forms_test.py ├── gallery_views_test.py ├── management_command_test.py ├── meta_test.py ├── meta_views_test.py ├── models_test.py ├── moderation_test.py ├── pagination_test.py ├── post_add_test.py ├── post_detail_test.py ├── post_image_renditions_test.py ├── post_media_sync_test.py ├── post_published_test.py ├── renditions_test.py ├── repository_test.py ├── settings.py ├── snippets_test.py ├── theme_test.py ├── transcript_views_test.py ├── upload_handler_test.py ├── urls.py ├── utils_test.py ├── video_dimensions_test.py ├── video_upload_test.py ├── video_views_test.py ├── wagtail_image_views_test.py └── widget_test.py └── tox.ini /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{py,rst,ini}] 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.{html,css,scss,json,yml}] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | 22 | [Makefile] 23 | indent_style = tab 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * Django Cast version: 2 | * Django version: 3 | * Python version: 4 | * Operating System: 5 | 6 | ### Description 7 | 8 | Describe what you were trying to get done. 9 | Tell us what happened, what went wrong, and what you expected to happen. 10 | 11 | ### What I Did 12 | 13 | ``` 14 | Paste the command(s) you ran and the output. 15 | If there was a crash, please include the traceback here. 16 | ``` 17 | -------------------------------------------------------------------------------- /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | name: Workflow for Codecov 2 | on: [push] 3 | jobs: 4 | run: 5 | runs-on: ${{ matrix.os }} 6 | strategy: 7 | matrix: 8 | os: [ubuntu-latest] 9 | env: 10 | OS: ${{ matrix.os }} 11 | PYTHON: '3.12' 12 | steps: 13 | - uses: actions/checkout@main 14 | - name: Install uv 15 | uses: astral-sh/setup-uv@v5 16 | 17 | - name: Setup Python 18 | run: uv python install 19 | 20 | - name: Install ffmpeg 21 | uses: FedericoCarboni/setup-ffmpeg@v2 22 | id: setup-ffmpeg 23 | - name: Generate coverage report 24 | run: | 25 | uv venv 26 | uv pip install -e .[dev] 27 | uv run python manage.py migrate 28 | uv run python -m pytest --cov=cast --cov-report=xml 29 | - name: Upload coverage to Codecov 30 | uses: codecov/codecov-action@v4 31 | with: 32 | env_vars: OS,PYTHON 33 | fail_ci_if_error: true 34 | flags: unittests 35 | name: codecov-umbrella 36 | token: ${{ secrets.CODECOV_TOKEN }} # configure in actions section of secrets 37 | verbose: true 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | __pycache__ 3 | 4 | # C extensions 5 | *.so 6 | 7 | # Packages 8 | *.egg 9 | *.egg-info 10 | dist 11 | build 12 | eggs 13 | parts 14 | bin 15 | var 16 | sdist 17 | develop-eggs 18 | .installed.cfg 19 | lib 20 | lib64 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | htmlcov 30 | 31 | # Translations 32 | *.mo 33 | 34 | # Mr Developer 35 | .mr.developer.cfg 36 | .project 37 | .pydevproject 38 | 39 | # Pycharm/Intellij 40 | .idea 41 | 42 | # Complexity 43 | output/*.html 44 | output/*/index.html 45 | 46 | # Sphinx 47 | docs/_build 48 | 49 | # environmen 50 | .env 51 | 52 | # gitignore 53 | .gitignore.swp 54 | 55 | # django files 56 | CACHE 57 | cast_images 58 | cast_videos 59 | django_cast.egg-info 60 | 61 | # domis hints for development 62 | hints/* 63 | 64 | # vscode 65 | .vscode/* 66 | *.code-workspace 67 | 68 | # vim 69 | .*swp 70 | .*swo 71 | 72 | # pypi 73 | .pypirc 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # tmp media 79 | tests/media/ 80 | 81 | # local databases 82 | db.sqlite3 83 | tests/test_database.sqlite3 84 | tests/test_database.sqlite3-journal 85 | 86 | # virtualenv 87 | .venv 88 | 89 | # uv 90 | 91 | uv.lock 92 | 93 | # example site media root 94 | example/media 95 | 96 | # jupyter notebooks 97 | .ipynb_checkpoints 98 | 99 | # macOS stuff 100 | .DS_Store 101 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.12" 7 | 8 | sphinx: 9 | configuration: docs/conf.py 10 | 11 | python: 12 | install: 13 | - requirements: docs/requirements.txt 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Config file for automatic testing at travis-ci.org 2 | 3 | language: python 4 | dist: bullseye 5 | 6 | python: 7 | - "3.11" 8 | 9 | before_install: 10 | - sudo apt-get -qq update 11 | - sudo apt-get install -y ffmpeg 12 | - pip install flit 13 | 14 | matrix: 15 | fast_finish: true 16 | 17 | # command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors 18 | install: flit install -s 19 | 20 | # command to run tests using coverage, e.g. python setup.py test 21 | script: coverage run --source cast runtests.py 22 | 23 | after_success: 24 | - codecov -e TOX_ENV 25 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Development Lead 6 | ---------------- 7 | 8 | * Jochen Wersdörfer 9 | 10 | Contributors 11 | ------------ 12 | 13 | * Dominik Geldmacher 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | BSD License 3 | 4 | Copyright (c) 2018, Jochen Wersdörfer 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without modification, 8 | are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, this 14 | list of conditions and the following disclaimer in the documentation and/or 15 | other materials provided with the distribution. 16 | 17 | * Neither the name of Django Cast nor the names of its 18 | contributors may be used to endorse or promote products derived from this 19 | software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 22 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 24 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 25 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 26 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 28 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 29 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 30 | OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | django: cd example && PYTHONUNBUFFERED=true $VIRTUAL_ENV/bin/python manage.py runserver 0.0.0.0:8000 2 | jupyterlab: $VIRTUAL_ENV/bin/python commands.py jupyterlab 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /cast/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django and Wagtail based blogging / podcasting package 3 | """ 4 | 5 | __version__ = "0.2.45" 6 | -------------------------------------------------------------------------------- /cast/admin_urls/audio.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from django.urls import path 4 | 5 | from ..views import audio as views 6 | 7 | urlpatterns: list[Any] = [ 8 | path("audio", views.index, name="index"), 9 | path(r"audio/add/", views.add, name="add"), 10 | path("audio/edit//", views.edit, name="edit"), 11 | path("audio/delete//", views.delete, name="delete"), 12 | path("audio/chooser/", views.chooser, name="chooser"), 13 | path("audio/chooser/upload/", views.chooser_upload, name="chooser_upload"), 14 | path("audio/chooser//", views.chosen, name="chosen"), 15 | ] 16 | -------------------------------------------------------------------------------- /cast/admin_urls/transcript.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from django.urls import path 4 | 5 | from ..views import transcript as views 6 | 7 | urlpatterns: list[Any] = [ 8 | path("transcript", views.index, name="index"), 9 | path(r"transcript/add/", views.add, name="add"), 10 | path("transcript/edit//", views.edit, name="edit"), 11 | path("transcript/delete//", views.delete, name="delete"), 12 | path("transcript/chooser/", views.chooser, name="chooser"), 13 | path("transcript/chooser/upload/", views.chooser_upload, name="chooser_upload"), 14 | path("transcript/chooser//", views.chosen, name="chosen"), 15 | ] 16 | -------------------------------------------------------------------------------- /cast/admin_urls/video.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from django.urls import path 4 | 5 | from ..views import video as views 6 | 7 | urlpatterns: list[Any] = [ 8 | path("video", views.index, name="index"), 9 | path(r"video/add/", views.add, name="add"), 10 | path("video/edit//", views.edit, name="edit"), 11 | path("video/delete//", views.delete, name="delete"), 12 | path("video/chooser/", views.chooser, name="chooser"), 13 | path("video/chooser/upload/", views.chooser_upload, name="chooser_upload"), 14 | path("video/chooser//", views.chosen, name="chosen"), 15 | ] 16 | -------------------------------------------------------------------------------- /cast/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ephes/django-cast/fa9f6a4e9f0ce3ecc1517f5032994e031ecc03a6/cast/api/__init__.py -------------------------------------------------------------------------------- /cast/api/urls.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from django.urls import include, path, re_path 4 | from rest_framework.schemas import get_schema_view 5 | 6 | from . import views 7 | 8 | app_name = "api" 9 | schema_view = get_schema_view(title="Cast API") 10 | 11 | urlpatterns: list[Any] = [ 12 | path("schema/", schema_view), 13 | path("", views.api_root, name="root"), 14 | # video 15 | path("videos/", views.VideoListView.as_view(), name="video_list"), 16 | re_path(r"^videos/(?P\d+)/?$", views.VideoDetailView.as_view(), name="video_detail"), 17 | path( 18 | "upload_video/", 19 | views.VideoCreateView.as_view(), 20 | name="upload_video", 21 | ), 22 | # audio 23 | path("audios/", views.AudioListView.as_view(), name="audio_list"), 24 | re_path(r"^audios/(?P\d+)/?$", views.AudioDetailView.as_view(), name="audio_detail"), 25 | re_path( 26 | r"^audios/podlove/(?P\d+)/(?:post/(?P\d+)/)?$", 27 | views.AudioPodloveDetailView.as_view(), 28 | name="audio_podlove_detail", 29 | ), 30 | path("audios/player_config/", views.PlayerConfig.as_view(), name="player_config"), 31 | # facet counts 32 | path("facet_counts/", views.FacetCountListView.as_view(), name="facet-counts-list"), 33 | re_path(r"facet_counts/(?P\d+)/?$", views.FacetCountsDetailView.as_view(), name="facet-counts-detail"), 34 | # comment training data 35 | path("comment_training_data/", views.CommentTrainingDataView.as_view(), name="comment-training-data"), 36 | # themes 37 | path("themes/", views.ThemeListView.as_view(), name="theme-list"), 38 | path("update_theme/", views.UpdateThemeView.as_view(), name="theme-update"), 39 | # wagtail api 40 | path("wagtail/", include((views.wagtail_api_router.get_urlpatterns(), "api"), namespace="wagtail")), 41 | ] 42 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /cast/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CastConfig(AppConfig): 5 | name: str = "cast" 6 | 7 | def ready(self) -> None: 8 | from .appsettings import init_cast_settings 9 | 10 | init_cast_settings() 11 | -------------------------------------------------------------------------------- /cast/cast_and_wagtail_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 | 5 | urlpatterns = [ 6 | path("cast/comments/", include("fluent_comments.urls")), 7 | path("cast/", include("cast.urls", namespace="cast")), 8 | path("cms/", include(wagtailadmin_urls)), 9 | path("", include(wagtail_urls)), 10 | ] 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /cast/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ephes/django-cast/fa9f6a4e9f0ce3ecc1517f5032994e031ecc03a6/cast/management/commands/__init__.py -------------------------------------------------------------------------------- /cast/management/commands/media_backup.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | from .storage_backend import get_production_and_backup_storage, sync_media_files 4 | 5 | 6 | class Command(BaseCommand): 7 | help = ( 8 | "backup media files from production to backup storage " 9 | "(requires Django >= 4.2 and production and backup storage configured)" 10 | ) 11 | 12 | @staticmethod 13 | def backup_media_files(production_storage, backup_storage): 14 | sync_media_files(production_storage, backup_storage) 15 | 16 | def handle(self, *args, **options): 17 | self.backup_media_files(*get_production_and_backup_storage()) 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /cast/management/commands/media_restore.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | from .storage_backend import get_production_and_backup_storage, sync_media_files 4 | 5 | 6 | class Command(BaseCommand): 7 | help = ( 8 | "restore media files from backup storage backend to production storage backend " 9 | "(requires Django >= 4.2 and production and backup storage configured)" 10 | ) 11 | 12 | @staticmethod 13 | def restore_media_files(production_storage, backup_storage): 14 | sync_media_files(backup_storage, production_storage) 15 | 16 | def handle(self, *args, **options): 17 | self.restore_media_files(*get_production_and_backup_storage()) 18 | -------------------------------------------------------------------------------- /cast/management/commands/media_sizes.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | from cast.utils import storage_walk_paths 4 | 5 | from .storage_backend import get_production_and_backup_storage 6 | 7 | 8 | class Command(BaseCommand): 9 | help = ( 10 | "show size of media files on production storage backend" 11 | "(requires Django >= 4.2 and production and backup storage configured)" 12 | ) 13 | 14 | @staticmethod 15 | def show_usage(paths): 16 | video_endings = {"mov", "mp4"} 17 | image_endings = {"jpg", "jpeg", "png"} 18 | image, video, misc = 0, 0, 0 19 | for path, size in paths.items(): 20 | ending = path.split(".")[-1].lower() 21 | if ending in video_endings: 22 | video += size 23 | elif ending in image_endings: 24 | image += size 25 | else: 26 | misc += size 27 | unit = 2**20 # MB 28 | print(f"video usage: {video / unit}") 29 | print(f"image usage: {image / unit}") 30 | print(f"misc usage: {misc / unit}") 31 | print(f"total usage: {sum(paths.values()) / unit}") 32 | 33 | @staticmethod 34 | def get_paths_with_sizes_for(storage_backend): 35 | paths = {} 36 | for path in storage_walk_paths(storage_backend): 37 | size = storage_backend.size(path) 38 | paths[path] = size 39 | print(path, size / 2**20) 40 | return paths 41 | 42 | def handle(self, *args, **options): 43 | production, _ = get_production_and_backup_storage() 44 | paths = self.get_paths_with_sizes_for(production) 45 | self.show_usage(paths) 46 | -------------------------------------------------------------------------------- /cast/management/commands/recalc_video_posters.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | from ...models import Video 4 | 5 | 6 | class Command(BaseCommand): 7 | help = "recalc the poster images for videos from the videos" 8 | 9 | def handle(self, *args, **options): 10 | for video in Video.objects.all(): 11 | # orig = video.original 12 | video.create_poster() 13 | video.save(poster=False) 14 | # break 15 | -------------------------------------------------------------------------------- /cast/management/commands/storage_backend.py: -------------------------------------------------------------------------------- 1 | from django.core.files.storage import InvalidStorageError, Storage 2 | 3 | try: 4 | from django.core.files.storage import storages # noqa F401 5 | 6 | DJANGO_VERSION_VALID = True 7 | except ImportError: # pragma: no cover 8 | DJANGO_VERSION_VALID = False 9 | 10 | from django.core.management.base import CommandError 11 | 12 | from ...utils import storage_walk_paths 13 | 14 | 15 | def sync_media_files(source_storage, target_storage): 16 | for num, path in enumerate(storage_walk_paths(source_storage)): 17 | if not target_storage.exists(path): 18 | with source_storage.open(path, "rb") as in_f: 19 | target_storage.save(path, in_f) 20 | if num % 100 == 0: # pragma: no cover 21 | print(".", end="", flush=True) 22 | 23 | 24 | def get_production_and_backup_storage() -> tuple[Storage, Storage]: 25 | if not DJANGO_VERSION_VALID: 26 | # make sure we run at least Django 4.2 27 | raise CommandError("Django version >= 4.2 is required") 28 | else: 29 | try: 30 | production_storage, backup_storage = storages["production"], storages["backup"] 31 | return production_storage, backup_storage 32 | except InvalidStorageError: 33 | raise CommandError("production or backup storage not configured") 34 | -------------------------------------------------------------------------------- /cast/migrations/0002_remove_blog_description.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2020-10-19 14:44 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("cast", "0001_initial"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name="blog", 15 | name="description", 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /cast/migrations/0003_remove_post_parent_blog.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2020-10-24 05:09 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("cast", "0002_remove_blog_description"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name="post", 15 | name="parent_blog", 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /cast/migrations/0009_alter_post_body.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.5 on 2021-07-22 09:47 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", "0008_auto_20210712_0919"), 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 | ("video", cast.blocks.VideoChooserBlock(icon="media", template="cast/video/video.html")), 30 | ] 31 | ), 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /cast/migrations/0010_rename_intro_blog_description.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.6 on 2021-08-06 14:50 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("cast", "0009_alter_post_body"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name="blog", 15 | old_name="intro", 16 | new_name="description", 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /cast/migrations/0012_alter_post_images.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.6 on 2021-08-18 08:16 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("wagtailimages", "0023_add_choose_permissions"), 10 | ("cast", "0011_alter_post_body"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="post", 16 | name="images", 17 | field=models.ManyToManyField(blank=True, to="wagtailimages.Image"), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /cast/migrations/0013_alter_gallery_images.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.6 on 2021-08-20 09:51 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("wagtailimages", "0023_add_choose_permissions"), 10 | ("cast", "0012_alter_post_images"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="gallery", 16 | name="images", 17 | field=models.ManyToManyField(to="wagtailimages.Image"), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /cast/migrations/0014_remove_gallery_user.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.6 on 2021-08-20 11:48 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("cast", "0013_alter_gallery_images"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name="gallery", 15 | name="user", 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /cast/migrations/0015_delete_blogindexpage.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.6 on 2021-08-21 06:09 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("wagtailredirects", "0006_redirect_increase_max_length"), 10 | ("wagtailcore", "0062_comment_models_and_pagesubscription"), 11 | ("wagtailforms", "0004_add_verbose_name_plural"), 12 | ("cast", "0014_remove_gallery_user"), 13 | ] 14 | 15 | operations = [ 16 | migrations.DeleteModel( 17 | name="BlogIndexPage", 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /cast/migrations/0016_auto_20210830_0422.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.6 on 2021-08-30 09:22 2 | 3 | import django.db.models.deletion 4 | import taggit.managers 5 | import wagtail.models.collections 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ("taggit", "0003_taggeditem_add_unique_index"), 13 | ("wagtailcore", "0062_comment_models_and_pagesubscription"), 14 | ("cast", "0015_delete_blogindexpage"), 15 | ] 16 | 17 | operations = [ 18 | migrations.AddField( 19 | model_name="audio", 20 | name="collection", 21 | field=models.ForeignKey( 22 | default=wagtail.models.collections.get_root_collection_id, 23 | on_delete=django.db.models.deletion.CASCADE, 24 | related_name="+", 25 | to="wagtailcore.collection", 26 | verbose_name="collection", 27 | ), 28 | ), 29 | migrations.AddField( 30 | model_name="audio", 31 | name="tags", 32 | field=taggit.managers.TaggableManager( 33 | blank=True, help_text=None, through="taggit.TaggedItem", to="taggit.Tag", verbose_name="tags" 34 | ), 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /cast/migrations/0018_alter_chaptermark_start.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.7 on 2021-09-21 08:16 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("cast", "0017_alter_post_body"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="chaptermark", 15 | name="start", 16 | field=models.TimeField(unique=True, verbose_name="Start time of chaptermark"), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /cast/migrations/0019_alter_chaptermark_start.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.7 on 2021-09-22 09:32 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("cast", "0018_alter_chaptermark_start"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="chaptermark", 15 | name="start", 16 | field=models.TimeField(verbose_name="Start time of chaptermark"), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /cast/migrations/0021_spamfilter.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.7 on 2021-10-01 09:21 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | import model_utils.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ("cast", "0020_auto_20210926_1556"), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name="SpamFilter", 17 | fields=[ 18 | ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), 19 | ( 20 | "created", 21 | model_utils.fields.AutoCreatedField( 22 | default=django.utils.timezone.now, editable=False, verbose_name="created" 23 | ), 24 | ), 25 | ( 26 | "modified", 27 | model_utils.fields.AutoLastModifiedField( 28 | default=django.utils.timezone.now, editable=False, verbose_name="modified" 29 | ), 30 | ), 31 | ("name", models.CharField(max_length=128, unique=True)), 32 | ("model", models.JSONField(default=dict, verbose_name="Spamfilter Model")), 33 | ], 34 | options={ 35 | "abstract": False, 36 | }, 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /cast/migrations/0022_alter_spamfilter_model.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.8 on 2021-10-05 09:29 2 | 3 | import cast.models.moderation 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("cast", "0021_spamfilter"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="spamfilter", 16 | name="model", 17 | field=models.JSONField( 18 | default=dict, encoder=cast.models.moderation.ModelEncoder, verbose_name="Spamfilter Model" 19 | ), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /cast/migrations/0023_alter_spamfilter_model.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0a1 on 2021-10-05 11:53 2 | 3 | import cast.models.moderation 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("cast", "0022_alter_spamfilter_model"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="spamfilter", 16 | name="model", 17 | field=models.JSONField( 18 | decoder=cast.models.moderation.ModelDecoder, 19 | default=dict, 20 | encoder=cast.models.moderation.ModelEncoder, 21 | verbose_name="Spamfilter Model", 22 | ), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /cast/migrations/0025_add_performance_indicators.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-01-04 13:19 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("cast", "0024_alter_homepage_body_alter_post_body"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="spamfilter", 15 | name="performance", 16 | field=models.JSONField(default=dict, verbose_name="Spamfilter Performance Indicators"), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /cast/migrations/0028_rename_and_drop_itune_fields.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-01-21 06:11 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("cast", "0027_episode"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name="post", 15 | name="block", 16 | ), 17 | migrations.RemoveField( 18 | model_name="post", 19 | name="explicit", 20 | ), 21 | migrations.RemoveField( 22 | model_name="post", 23 | name="keywords", 24 | ), 25 | migrations.RemoveField( 26 | model_name="post", 27 | name="podcast_audio", 28 | ), 29 | migrations.RenameField( 30 | model_name="episode", 31 | old_name="new_block", 32 | new_name="block", 33 | ), 34 | migrations.RenameField( 35 | model_name="episode", 36 | old_name="new_explicit", 37 | new_name="explicit", 38 | ), 39 | migrations.RenameField( 40 | model_name="episode", 41 | old_name="new_keywords", 42 | new_name="keywords", 43 | ), 44 | migrations.RenameField( 45 | model_name="episode", 46 | old_name="new_podcast_audio", 47 | new_name="podcast_audio", 48 | ), 49 | ] 50 | -------------------------------------------------------------------------------- /cast/migrations/0029_add_metadata_field_to_audio.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-01-28 05:43 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("cast", "0028_rename_and_drop_itune_fields"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="audio", 15 | name="data", 16 | field=models.JSONField(blank=True, default=dict, verbose_name="Metadata"), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /cast/migrations/0030_remove_pub_date_is_handled_by_wagtail.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.4 on 2023-02-02 21:49 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("cast", "0029_add_metadata_field_to_audio"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name="post", 15 | name="pub_date", 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /cast/migrations/0031_remove_timestampedmodel_because_wagtail.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.4 on 2023-02-03 05:45 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("cast", "0030_remove_pub_date_is_handled_by_wagtail"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name="post", 15 | name="created", 16 | ), 17 | migrations.RemoveField( 18 | model_name="post", 19 | name="modified", 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /cast/migrations/0032_remove_timestampedmodel_because_wagtail.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.4 on 2023-02-03 06:04 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("cast", "0031_remove_timestampedmodel_because_wagtail"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name="blog", 15 | name="created", 16 | ), 17 | migrations.RemoveField( 18 | model_name="blog", 19 | name="modified", 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /cast/migrations/0036_alter_blog_author.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.4 on 2023-02-04 16:27 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("cast", "0035_remove_new_prefix_podcast_fields"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="blog", 15 | name="author", 16 | field=models.CharField( 17 | blank=True, 18 | default=None, 19 | help_text="Freeform text that will be used in the feed.", 20 | max_length=255, 21 | null=True, 22 | ), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /cast/migrations/0038_alter_episode_keywords.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.6 on 2023-02-07 15:28 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("cast", "0037_alter_episode_block_alter_episode_explicit_and_more"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="episode", 14 | name="keywords", 15 | field=models.CharField( 16 | blank=True, 17 | default="", 18 | help_text="A comma-delimited-list of up to 12 words for iTunes\n searches. Perhaps include misspellings of the title.", 19 | max_length=255, 20 | ), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /cast/migrations/0039_blog_noindex.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.7 on 2023-03-04 08:35 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("cast", "0038_alter_episode_keywords"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="blog", 14 | name="noindex", 15 | field=models.BooleanField( 16 | default=False, 17 | help_text="Whether to add a noindex meta tag to this page and all subpages.", 18 | verbose_name="noindex", 19 | ), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /cast/migrations/0040_alter_blog_noindex.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.7 on 2023-03-05 21:36 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("cast", "0039_blog_noindex"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="blog", 14 | name="noindex", 15 | field=models.BooleanField( 16 | default=False, 17 | help_text="Whether to add a noindex meta tag to this page and all subpages excluding them from search engines.", 18 | verbose_name="noindex", 19 | ), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /cast/migrations/0041_templatebasedirectory.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.7 on 2023-03-05 21:47 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("wagtailcore", "0083_workflowcontenttype"), 10 | ("cast", "0040_alter_blog_noindex"), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name="TemplateBaseDirectory", 16 | fields=[ 17 | ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), 18 | ( 19 | "name", 20 | models.CharField( 21 | choices=[("bootstrap4", "Bootstrap 4"), ("plain", "Just HTML")], 22 | default="bootstrap4", 23 | max_length=10, 24 | ), 25 | ), 26 | ( 27 | "site", 28 | models.OneToOneField( 29 | editable=False, on_delete=django.db.models.deletion.CASCADE, to="wagtailcore.site" 30 | ), 31 | ), 32 | ], 33 | options={ 34 | "abstract": False, 35 | }, 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /cast/migrations/0042_blog_template_base_dir.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.7 on 2023-03-12 21:20 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("cast", "0041_templatebasedirectory"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="blog", 14 | name="template_base_dir", 15 | field=models.CharField( 16 | choices=[("bootstrap4", "Bootstrap 4"), ("plain", "Just HTML")], default="bootstrap4", max_length=10 17 | ), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /cast/migrations/0043_alter_blog_template_base_dir.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.7 on 2023-03-12 21:41 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("cast", "0042_blog_template_base_dir"), 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 | max_length=10, 20 | null=True, 21 | ), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /cast/migrations/0044_alter_blog_template_base_dir_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.7 on 2023-03-12 22:05 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("cast", "0043_alter_blog_template_base_dir"), 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 determine 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.", 31 | max_length=128, 32 | ), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /cast/migrations/0048_added_visible_date_index_for_wagtail_api.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.8 on 2023-05-19 11:23 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("cast", "0047_alter_episode_podcast_audio"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="post", 15 | name="visible_date", 16 | field=models.DateTimeField( 17 | db_index=True, 18 | default=django.utils.timezone.now, 19 | help_text="The visible date of the post which is used for sorting.", 20 | ), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /cast/migrations/0049_added_category_snippets.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.4 on 2023-08-15 11:58 2 | 3 | from django.db import migrations, models 4 | import modelcluster.fields 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("cast", "0048_added_visible_date_index_for_wagtail_api"), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="PostCategory", 15 | fields=[ 16 | ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), 17 | ("name", models.CharField(help_text="The name for this category", max_length=255, unique=True)), 18 | ( 19 | "slug", 20 | models.SlugField( 21 | help_text="A slug to identify posts by this category", unique=True, verbose_name="slug" 22 | ), 23 | ), 24 | ], 25 | options={ 26 | "verbose_name": "Post Category", 27 | "verbose_name_plural": "Post Categories", 28 | "ordering": ["name"], 29 | }, 30 | ), 31 | migrations.AddField( 32 | model_name="post", 33 | name="categories", 34 | field=modelcluster.fields.ParentalManyToManyField(blank=True, to="cast.postcategory"), 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /cast/migrations/0054_alter_blog_template_base_dir_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2023-12-31 07:56 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("cast", "0053_rename_default_layout"), 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 | -------------------------------------------------------------------------------- /cast/migrations/0055_alter_podcast_itunes_artwork.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.3 on 2024-03-13 22:09 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", "0054_alter_blog_template_base_dir_and_more"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="podcast", 16 | name="itunes_artwork", 17 | field=models.ForeignKey( 18 | blank=True, 19 | null=True, 20 | on_delete=django.db.models.deletion.SET_NULL, 21 | related_name="podcasts", 22 | to="cast.itunesartwork", 23 | ), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /cast/migrations/0057_rename_cover_image_and_add_alt_text.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.4 on 2024-05-08 20:18 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("cast", "0056_add_cover_image_for_post"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name="post", 15 | old_name="cover", 16 | new_name="cover_image", 17 | ), 18 | migrations.AddField( 19 | model_name="post", 20 | name="cover_alt_text", 21 | field=models.CharField(blank=True, default="", max_length=255), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /cast/migrations/0061_alter_audio_options.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.5 on 2025-02-04 10:07 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("cast", "0060_transcript"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name="audio", 15 | options={"ordering": ("-created",)}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /cast/migrations/0062_alter_transcript_options.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.4 on 2025-02-23 08:14 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("cast", "0061_alter_audio_options"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name="transcript", 15 | options={"ordering": ("-id",)}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /cast/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ephes/django-cast/fa9f6a4e9f0ce3ecc1517f5032994e031ecc03a6/cast/migrations/__init__.py -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /cast/models/file.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.db import models 3 | from model_utils.models import TimeStampedModel 4 | 5 | 6 | class File(TimeStampedModel): 7 | user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) 8 | original = models.FileField(upload_to="cast_files/") 9 | 10 | def get_all_paths(self) -> set[str]: 11 | paths = set() 12 | paths.add(self.original.name) 13 | return paths 14 | -------------------------------------------------------------------------------- /cast/models/itunes.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from model_utils.models import TimeStampedModel 3 | 4 | 5 | class ItunesArtWork(TimeStampedModel): # type: ignore 6 | original = models.ImageField( 7 | upload_to="cast_images/itunes_artwork", 8 | height_field="original_height", 9 | width_field="original_width", 10 | ) 11 | original_height = models.PositiveIntegerField(blank=True, null=True) 12 | original_width = models.PositiveIntegerField(blank=True, null=True) 13 | -------------------------------------------------------------------------------- /cast/models/snippets.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from wagtail.snippets.models import register_snippet 3 | 4 | 5 | @register_snippet 6 | class PostCategory(models.Model): 7 | """Post category snippet for grouping posts.""" 8 | 9 | name = models.CharField(max_length=255, unique=True, help_text="The name for this category") 10 | slug = models.SlugField(verbose_name="slug", unique=True, help_text="A slug to identify posts by this category") 11 | 12 | class Meta: 13 | verbose_name = "Post Category" 14 | verbose_name_plural = "Post Categories" 15 | ordering = ["name"] 16 | 17 | def __str__(self) -> str: 18 | return self.name 19 | -------------------------------------------------------------------------------- /cast/moderation.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from django.http import HttpRequest 4 | from fluent_comments.models import FluentComment 5 | from fluent_comments.moderation import FluentCommentsModerator 6 | 7 | from .models import SpamFilter 8 | 9 | 10 | class Moderator(FluentCommentsModerator): 11 | def __init__(self, model, spamfilter=None): 12 | super().__init__(model) 13 | # Allow spamfilter to be set for tests 14 | if spamfilter is not None: 15 | self.spamfilter = spamfilter 16 | else: 17 | self.spamfilter = SpamFilter.get_default() 18 | 19 | def allow(self, comment: FluentComment, content_object: Any, request: HttpRequest) -> bool: 20 | """ 21 | Allow all. Just mark moderated comments as 'is_removed' but 22 | keep them in the database. Even awful comments are useful as 23 | a bad training example :). 24 | """ 25 | return True 26 | 27 | def moderate(self, comment: FluentComment, content_object: Any, request: HttpRequest) -> bool: 28 | message = SpamFilter.comment_to_message(comment) 29 | if self.spamfilter is not None: 30 | predicted_label = self.spamfilter.model.predict_label(message) 31 | else: 32 | predicted_label = "unknown" 33 | if predicted_label == "spam": 34 | comment.is_removed, comment.is_public = True, False 35 | return True 36 | else: 37 | comment.is_removed, comment.is_public = False, True 38 | return False 39 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /cast/static/cast/css/my_div.css: -------------------------------------------------------------------------------- 1 | #myDiv { 2 | margin: 0 auto; 3 | border: 1px solid black; 4 | height: 85vh; 5 | min-height: 300px; 6 | } 7 | -------------------------------------------------------------------------------- /cast/static/cast/css/plain/cast.css: -------------------------------------------------------------------------------- 1 | /* Styles for plain templates */ 2 | 3 | .cast-date-facet-container { 4 | display: flex; 5 | flex-wrap: wrap; 6 | height: auto; 7 | } 8 | 9 | .cast-date-facet-item { 10 | margin-right: 10px; 11 | margin-left: 10px; 12 | } 13 | 14 | .cast-page-navigation { 15 | text-align: center; 16 | } 17 | 18 | .cast-pagination { 19 | display: flex; 20 | justify-content: center; 21 | list-style-type: none; 22 | padding: 0; 23 | } 24 | 25 | .cast-page-item { 26 | margin-right: 10px; 27 | } 28 | 29 | .cast-page-link { 30 | color: #007bff; 31 | } 32 | 33 | .cast-page-link-disabled { 34 | color: #6c757d; 35 | } 36 | 37 | .cast-page-link-active { 38 | background-color: #007bff; 39 | color: #fff; 40 | } 41 | -------------------------------------------------------------------------------- /cast/static/cast/img/Audio-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml 23 | 26 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /cast/static/cast/img/Feed-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /cast/static/cast/js/wagtail/audio-chooser-telepath.js: -------------------------------------------------------------------------------- 1 | class AudioChooser { 2 | constructor(html, idPattern) { 3 | this.html = html; 4 | this.idPattern = idPattern; 5 | } 6 | 7 | render(placeholder, name, id, initialState) { 8 | const html = this.html.replace(/__NAME__/g, name).replace(/__ID__/g, id); 9 | // eslint-disable-next-line no-param-reassign 10 | placeholder.outerHTML = html; 11 | /* the chooser object returned by createVideoChooser also serves as the JS widget representation */ 12 | // eslint-disable-next-line no-undef 13 | const chooser = createAudioChooser(id); 14 | chooser.setState(initialState); 15 | return chooser; 16 | } 17 | } 18 | window.telepath.register('cast.wagtail.AudioChooser', AudioChooser); 19 | -------------------------------------------------------------------------------- /cast/static/cast/js/wagtail/video-chooser-telepath.js: -------------------------------------------------------------------------------- 1 | class VideoChooser { 2 | constructor(html, idPattern) { 3 | this.html = html; 4 | this.idPattern = idPattern; 5 | } 6 | 7 | render(placeholder, name, id, initialState) { 8 | const html = this.html.replace(/__NAME__/g, name).replace(/__ID__/g, id); 9 | // eslint-disable-next-line no-param-reassign 10 | placeholder.outerHTML = html; 11 | /* the chooser object returned by createVideoChooser also serves as the JS widget representation */ 12 | // eslint-disable-next-line no-undef 13 | const chooser = createVideoChooser(id); 14 | chooser.setState(initialState); 15 | return chooser; 16 | } 17 | } 18 | window.telepath.register('cast.wagtail.VideoChooser', VideoChooser); 19 | -------------------------------------------------------------------------------- /cast/static/cast/vite/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "src/audio/podlove-player.ts": { 3 | "file": "podlovePlayer-BhApzi9J.js", 4 | "name": "podlovePlayer", 5 | "src": "src/audio/podlove-player.ts", 6 | "isEntry": true 7 | }, 8 | "src/gallery/image-gallery-bs4.ts": { 9 | "file": "main-CCUgFhRa.js", 10 | "name": "main", 11 | "src": "src/gallery/image-gallery-bs4.ts", 12 | "isEntry": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /cast/templates/cast/audio/add.html: -------------------------------------------------------------------------------- 1 | {% extends "wagtailadmin/base.html" %} 2 | {% load i18n %} 3 | {% load wagtailimages_tags %} 4 | {% block titletag %} 5 | {% trans "Add audio" %} 6 | {% endblock %} 7 | 8 | {% block extra_js %} 9 | {{ block.super }} 10 | {{ form.media.js }} 11 | 12 | {% url 'wagtailadmin_tag_autocomplete' as autocomplete_url %} 13 | 20 | {% endblock %} 21 | 22 | {% block extra_css %} 23 | {{ block.super }} 24 | {{ form.media.css }} 25 | {% endblock %} 26 | 27 | {% block content %} 28 | {% trans "Add audio" as add_str %} 29 | {% include "wagtailadmin/shared/header.html" with title=add_str icon="media" %} 30 | 31 |
32 |
33 | {% csrf_token %} 34 |
    35 | {% for field in form %} 36 |
  • 37 | {% include "wagtailadmin/shared/field.html" %} 38 |
  • 39 | {% endfor %} 40 |
  • 41 | 42 |
  • 43 |
44 |
45 |
46 | {% endblock %} 47 | -------------------------------------------------------------------------------- /cast/templates/cast/audio/audio.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {% if page.pk %} 3 | {% if not render_for_feed %} 4 | 11 | 12 | {% endif %} 13 | {% endif %} 14 | -------------------------------------------------------------------------------- /cast/templates/cast/audio/chooser.html: -------------------------------------------------------------------------------- 1 | {% extends "wagtailadmin/widgets/chooser.html" %} 2 | {% block chooser_class %}audio-chooser{% endblock %} 3 | 4 | {% block chosen_state_view %} 5 | {{ title }} 6 | {% endblock %} 7 | 8 | {% block edit_chosen_item_url %}{{ edit_url }}{% endblock %} 9 | -------------------------------------------------------------------------------- /cast/templates/cast/audio/chooser_results.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% if audios %} 3 | {% if is_searching %} 4 |

5 | {% blocktrans count counter=audios.paginator.count %} 6 | There is one match 7 | {% plural %} 8 | There are {{ counter }} matches 9 | {% endblocktrans %} 10 |

11 | {% else %} 12 |

{% trans "Latest audio" %}

13 | {% endif %} 14 | 15 | {% include "cast/audio/list.html" with choosing=1 %} 16 | 17 | {% include pagination_template with items=audios is_ajax=1 %} 18 | {% else %} 19 |

{% blocktrans %}Sorry, no audio models match "{{ query_string }}"{% endblocktrans %}

20 | {% endif %} 21 | -------------------------------------------------------------------------------- /cast/templates/cast/audio/confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "wagtailadmin/base.html" %} 2 | {% load wagtailadmin_tags i18n %} 3 | {% block titletag %}{% blocktrans with title=audio.title %}Delete {{ title }}{% endblocktrans %}{% endblock %} 4 | {% block content %} 5 | {% trans "Delete audio" as del_str %} 6 | {% include "wagtailadmin/shared/header.html" with title=del_str subtitle=audio.title icon="media" %} 7 | 8 |
9 |

{% trans "Are you sure you want to delete this audio?" %}

10 |
11 | {% csrf_token %} 12 | 13 |
14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /cast/templates/cast/audio/results.html: -------------------------------------------------------------------------------- 1 | {% load i18n wagtailadmin_tags %} 2 | 3 | {% if audios %} 4 | {% if is_searching %} 5 |

6 | {% blocktrans count counter=audios|length %} 7 | There is one match 8 | {% plural %} 9 | There are {{ counter }} matches 10 | {% endblocktrans %} 11 |

12 | 13 | {% search_other %} 14 | {% endif %} 15 | 16 | {% include "cast/audio/list.html" %} 17 | 18 | {% include "wagtailadmin/shared/pagination_nav.html" with items=audios is_searching=is_searching linkurl="castaudio:index" %} 19 | {% else %} 20 | {% if is_searching %} 21 |

{% blocktrans %}Sorry, no audio files match "{{ query_string }}"{% endblocktrans %}

22 | 23 | {% search_other %} 24 | {% else %} 25 | {% url 'castaudio:add' as castaudio_add_audio_url %} 26 | {% if current_collection %} 27 |

{% blocktrans %}You haven't uploaded any audios in this collection. You can upload audio files.{% endblocktrans %}

28 | {% else %} 29 |

{% blocktrans %}You haven't uploaded any audios. You can upload audio files.{% endblocktrans %}

30 | {% endif %} 31 | {% endif %} 32 | {% endif %} 33 | -------------------------------------------------------------------------------- /cast/templates/cast/base.html: -------------------------------------------------------------------------------- 1 | 2 | {% comment %} 3 | As the developer of this package, don't place anything here if you can help it 4 | since this allows developers to have interoperability between your template 5 | structure and their own. 6 | 7 | Example: Developer melding the 2SoD pattern to fit inside with another pattern:: 8 | 9 | {% extends "base.html" %} 10 | {% load static %} 11 | 12 | 13 | {% block extra_js %} 14 | 15 | 16 | {% block javascript %} 17 | 18 | {% endblock javascript %} 19 | 20 | {% endblock extra_js %} 21 | {% endcomment %} 22 | -------------------------------------------------------------------------------- /cast/templates/cast/bootstrap4/400.html: -------------------------------------------------------------------------------- 1 | {% extends "./base.html" %} 2 | 3 | {% block title %}Bad Request{% endblock %} 4 | 5 | {% block content %} 6 |

Bad request!

7 | 8 |

Something in your request was just not right.

9 | {% endblock content %} 10 | -------------------------------------------------------------------------------- /cast/templates/cast/bootstrap4/403.html: -------------------------------------------------------------------------------- 1 | {% extends "./base.html" %} 2 | 3 | {% block title %}Permission Denied{% endblock %} 4 | 5 | {% block content %} 6 |

Permission Denied!

7 | 8 |

Authorization failed.

9 | {% endblock content %} 10 | -------------------------------------------------------------------------------- /cast/templates/cast/bootstrap4/403_csrf.html: -------------------------------------------------------------------------------- 1 | {% extends "./base.html" %} 2 | 3 | {% block title %}Forbidden (403){% endblock %} 4 | 5 | {% block content %} 6 |

Forbidden (403)

7 | 8 |

CSRF verification failed. Request aborted.

9 | {% endblock content %} 10 | -------------------------------------------------------------------------------- /cast/templates/cast/bootstrap4/404.html: -------------------------------------------------------------------------------- 1 | {% extends "./base.html" %} 2 | 3 | {% block title %}Page not found{% endblock %} 4 | 5 | {% block content %} 6 |

Page not found

7 | 8 |

This is not the page you were looking for.

9 | {% endblock content %} 10 | -------------------------------------------------------------------------------- /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.

11 | {% endblock content %} 12 | -------------------------------------------------------------------------------- /cast/templates/cast/bootstrap4/_list_of_posts_and_paging_controls.html: -------------------------------------------------------------------------------- 1 |
2 | {% for post in posts %} 3 | {% include "./post_body.html" with render_detail=False page=post %} 4 |
5 | {% endfor %} 6 | 16 |
17 | 18 |
19 |

{% include "./pagination.html" %}

20 |
21 | -------------------------------------------------------------------------------- /cast/templates/cast/bootstrap4/episode.html: -------------------------------------------------------------------------------- 1 | {% extends "./post.html" %} 2 | 3 | {% block meta %} 4 | {% if episode.podcast_audio %} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 29 | {% else %} 30 | {{ block.super }} 31 | {% endif %} 32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /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 |

12 | 13 | , 14 | {{ page.owner.username | title}} 15 |
16 | {% for block in page.body %} 17 | {% if block.block_type != "detail" or render_detail %} 18 |
19 | {% for block in block.value %} 20 |
21 | {% include_block block %} 22 |
23 | {% endfor %} 24 |
25 | {% if not render_detail %} 26 | {% block detail_link %}{% endblock %} 27 | {% endif %} 28 | {% endif %} 29 | {% endfor %} 30 |
31 | -------------------------------------------------------------------------------- /cast/templates/cast/bootstrap4/select_theme.html: -------------------------------------------------------------------------------- 1 | 24 | -------------------------------------------------------------------------------- /cast/templates/cast/bootstrap4/transcript.html: -------------------------------------------------------------------------------- 1 | {% extends "./base.html" %} 2 | 3 | {% block content %} 4 | {% for segment in transcript.transcripts %} 5 |
6 | {{ segment.start }} 7 | {{ segment.text }} 8 | {{ segment.end }} 9 |
10 | {% endfor %} 11 | {% endblock content %} 12 | -------------------------------------------------------------------------------- /cast/templates/cast/cast_base.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load static %} 4 | 5 | {% block css %} 6 | {{ block.super }} 7 | 8 | {% endblock css %} 9 | -------------------------------------------------------------------------------- /cast/templates/cast/home_page.html: -------------------------------------------------------------------------------- 1 | {% extends "cast/cast_base.html" %} 2 | {% load i18n %} 3 | {% load static %} 4 | 5 | {% load wagtailcore_tags %} 6 | {% load wagtailimages_tags %} 7 | 8 | {% block body_class %}template-homepage{% endblock %} 9 | 10 | {% block content %} 11 |
12 | {% for block in page.body %} 13 |
14 | {% include_block block %} 15 |
16 | {% endfor %} 17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /cast/templates/cast/image/image.html: -------------------------------------------------------------------------------- 1 | {% load wagtailimages_tags %} 2 | 3 | 4 | 9 | {{ value.default_alt_text }} 18 | 19 | 20 | -------------------------------------------------------------------------------- /cast/templates/cast/image_form.html: -------------------------------------------------------------------------------- 1 | {% extends "cast/cast_base.html" %} 2 | {% load static %} 3 | 4 | {% block content %} 5 |
6 |
7 | {% csrf_token %} 8 | {{ form.as_p }} 9 |

10 |
11 | {% endblock content %} 12 | -------------------------------------------------------------------------------- /cast/templates/cast/plain/400.html: -------------------------------------------------------------------------------- 1 | {% extends "./base.html" %} 2 | 3 | {% block title %}Page not found{% endblock %} 4 | 5 | {% block content %} 6 |

Bad request!

7 | 8 |

Something in your request was just not right.

9 | {% endblock content %} 10 | -------------------------------------------------------------------------------- /cast/templates/cast/plain/403.html: -------------------------------------------------------------------------------- 1 | {% extends "./base.html" %} 2 | 3 | {% block title %}Page not found{% endblock %} 4 | 5 | {% block content %} 6 |

Permission Denied!

7 | 8 |

Authorization failed.

9 | {% endblock content %} 10 | -------------------------------------------------------------------------------- /cast/templates/cast/plain/403_csrf.html: -------------------------------------------------------------------------------- 1 | {% extends "./base.html" %} 2 | 3 | {% block title %}Forbidden (403){% endblock %} 4 | 5 | {% block content %} 6 |

Forbidden (403)

7 | 8 |

CSRF verification failed. Request aborted.

9 | {% endblock content %} 10 | -------------------------------------------------------------------------------- /cast/templates/cast/plain/404.html: -------------------------------------------------------------------------------- 1 | {% extends "./base.html" %} 2 | 3 | {% block title %}Page not found{% endblock %} 4 | 5 | {% block content %} 6 |

Page not found

7 | 8 |

This is not the page you were looking for.

9 | {% endblock content %} 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /cast/templates/cast/plain/_filter_form.html: -------------------------------------------------------------------------------- 1 | {{ form.non_field_errors }} 2 |
3 |

4 | 5 | {{ form.search }} 6 |

7 |

8 | 9 | 10 | - 11 |

12 |

13 | {{ form.date_facets.errors }} 14 | 15 | {{ form.date_facets }} 16 |

17 |

18 | 19 | 24 |

25 | 26 |
27 | -------------------------------------------------------------------------------- /cast/templates/cast/plain/_list_of_posts_and_paging_controls.html: -------------------------------------------------------------------------------- 1 |
2 | {% include "./pagination.html" %} 3 | {% for post in posts %} 4 | {% include "./post_body.html" with render_detail=False page=post %} 5 | {% endfor %} 6 |
7 | 8 |
9 | {% include "./pagination.html" %} 10 |
11 | -------------------------------------------------------------------------------- /cast/templates/cast/plain/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | {% block meta %} 6 | 7 | 8 | 9 | {% block robots %} 10 | 11 | {% endblock robots %} 12 | {% block title %} 13 | Title of my Site! 14 | {% endblock title %} 15 | {% block description %} 16 | 17 | {% endblock description %} 18 | {% endblock %} 19 | 20 | {% block css %} 21 | 22 | {% endblock %} 23 | 24 | 25 | {% block header %} 26 |
The header
27 | {% endblock header %} 28 | {% block navigation %} 29 | 30 | {% endblock navigation %} 31 |
32 | {% block main %} 33 |
34 |

Use this document as a way to quick start any new project.

35 |
36 | {% endblock main %} 37 | {% block footer %} 38 |
The footer
39 | {% endblock footer %} 40 |
41 | {% block javascript %} 42 | 43 | {% endblock javascript %} 44 | 45 | 46 | -------------------------------------------------------------------------------- /cast/templates/cast/plain/blog_list_of_posts.html: -------------------------------------------------------------------------------- 1 | {% extends "./base.html" %} 2 | {% load wagtailcore_tags %} 3 | {% load static %} 4 | 5 | {% block robots %} 6 | {% if blog.noindex %} 7 | 8 | {% else %} 9 | 10 | {% endif %} 11 | {% endblock robots %} 12 | 13 | {% block title %} 14 | {{ page.seo_title }} 15 | {% endblock %} 16 | 17 | {% block description %} 18 | 19 | {% endblock %} 20 | 21 | {% block header %} 22 |
23 |

{{ page.title }}

24 |
25 |

Description

26 | {{ page.description|richtext }} 27 |
28 |
29 |

Feed

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 |
58 | {% include "./pagination.html" %} 59 |
60 | {% endblock footer %} 61 | -------------------------------------------------------------------------------- /cast/templates/cast/plain/episode.html: -------------------------------------------------------------------------------- 1 | {% extends "./post.html" %} 2 | 3 | {% block meta %} 4 | {{ block.super }} 5 | {% if episode.podcast_audio %} 6 | 7 | 8 | {% endif %} 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /cast/templates/cast/plain/gallery_modal.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ephes/django-cast/fa9f6a4e9f0ce3ecc1517f5032994e031ecc03a6/cast/templates/cast/plain/gallery_modal.html -------------------------------------------------------------------------------- /cast/templates/cast/plain/post.html: -------------------------------------------------------------------------------- 1 | {% extends "./base.html" %} 2 | {% load i18n %} 3 | {% load static %} 4 | 5 | {% load wagtailcore_tags %} 6 | {% load wagtailimages_tags %} 7 | 8 | {% if page.comments_enabled %} 9 | {% load comments %} 10 | {% endif %} 11 | 12 | {% block css %} 13 | {{ block.super }} 14 | {% if page.comments_are_enabled %} 15 | 16 | {% endif %} 17 | {% endblock css %} 18 | 19 | {% block robots %} 20 | {% if blog.noindex %} 21 | 22 | {% else %} 23 | 24 | {% endif %} 25 | {% endblock robots %} 26 | 27 | {% block title %}{{ page.title }}{% endblock title %} 28 | 29 | {% block main %} 30 | {% include "./post_body.html" with render_detail=True %} 31 | 32 | {% if page.comments_are_enabled %} 33 | {% render_comment_list for page %} 34 | {% render_comment_form for page %} 35 | {% endif %} 36 | 37 |

Return to blog

38 | 39 | {% endblock %} 40 | -------------------------------------------------------------------------------- /cast/templates/cast/plain/post_body.html: -------------------------------------------------------------------------------- 1 | {% load wagtailcore_tags %} 2 | 3 |
4 |
5 |

{{ page.title }}

6 | 7 | , 8 | 9 | {{ page.owner.username | title}} 10 |
11 | {% for block in page.body %} 12 | {% if block.block_type != "detail" or render_detail %} 13 |
14 | {% include_block block %} 15 |
16 | {% block detail_link %}{% endblock %} 17 | {% endif %} 18 | {% endfor %} 19 |
20 | -------------------------------------------------------------------------------- /cast/templates/cast/plain/select_theme.html: -------------------------------------------------------------------------------- 1 |
2 | {% csrf_token %} 3 | {{ theme_form.as_p }} 4 | 5 |
6 | -------------------------------------------------------------------------------- /cast/templates/cast/transcript/add.html: -------------------------------------------------------------------------------- 1 | {% extends "wagtailadmin/base.html" %} 2 | {% load i18n %} 3 | {% load wagtailimages_tags %} 4 | {% block titletag %} 5 | {% trans "Add transcript" %} 6 | {% endblock %} 7 | 8 | {% block extra_js %} 9 | {{ block.super }} 10 | {{ form.media.js }} 11 | 12 | {% url 'wagtailadmin_tag_autocomplete' as autocomplete_url %} 13 | 20 | {% endblock %} 21 | 22 | {% block extra_css %} 23 | {{ block.super }} 24 | {{ form.media.css }} 25 | {% endblock %} 26 | 27 | {% block content %} 28 | {% trans "Add transcript" as add_str %} 29 | {% include "wagtailadmin/shared/header.html" with title=add_str icon="media" %} 30 | 31 |
32 |
33 | {% csrf_token %} 34 |
    35 | {% for field in form %} 36 |
  • 37 | {% include "wagtailadmin/shared/field.html" %} 38 |
  • 39 | {% endfor %} 40 |
  • 41 | 42 |
  • 43 |
44 |
45 |
46 | {% endblock %} 47 | -------------------------------------------------------------------------------- /cast/templates/cast/transcript/chooser.html: -------------------------------------------------------------------------------- 1 | {% extends "wagtailadmin/widgets/chooser.html" %} 2 | {% block chooser_class %}media-chooser{% endblock %} 3 | 4 | {% block chosen_state_view %} 5 | {{ title }} 6 | {% endblock %} 7 | 8 | {% block edit_chosen_item_url %}{{ edit_url }}{% endblock %} 9 | -------------------------------------------------------------------------------- /cast/templates/cast/transcript/chooser_results.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% if transcripts %} 3 | {% if is_searching %} 4 |

5 | {% blocktrans count counter=transcripts.paginator.count %} 6 | There is one match 7 | {% plural %} 8 | There are {{ counter }} matches 9 | {% endblocktrans %} 10 |

11 | {% else %} 12 |

{% trans "Latest transcript" %}

13 | {% endif %} 14 | 15 | {% include "cast/transcript/list.html" with choosing=1 %} 16 | 17 | {% include pagination_template with items=transcripts is_ajax=1 %} 18 | {% else %} 19 |

{% blocktrans %}Sorry, no transcript models match "{{ query_string }}"{% endblocktrans %}

20 | {% endif %} 21 | -------------------------------------------------------------------------------- /cast/templates/cast/transcript/confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "wagtailadmin/base.html" %} 2 | {% load wagtailadmin_tags i18n %} 3 | {% block titletag %}{% blocktrans with title=transcript.pk %}Delete {{ title }}{% endblocktrans %}{% endblock %} 4 | {% block content %} 5 | {% trans "Delete transcript" as del_str %} 6 | {% include "wagtailadmin/shared/header.html" with title=del_str subtitle=transcript.pk icon="media" %} 7 | 8 |
9 |

{% trans "Are you sure you want to delete this transcript?" %}

10 |
11 | {% csrf_token %} 12 | 13 |
14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /cast/templates/cast/transcript/list.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 17 | 18 | 19 | 20 | 21 | {% for transcript in transcripts %} 22 | 23 | 30 | 33 | 34 | {% endfor %} 35 | 36 |
9 | {% if not is_searching %} 10 | 11 | {% trans "ID" %} 12 | 13 | {% else %} 14 | {% trans "ID" %} 15 | {% endif %} 16 | {% trans "Audio title" %}
24 | {% if choosing %} 25 |

{{ transcript.id }}

26 | {% else %} 27 |

{{ transcript.id }}

28 | {% endif %} 29 |
31 | {{ transcript.audio.title }} 32 |
37 | -------------------------------------------------------------------------------- /cast/templates/cast/transcript/results.html: -------------------------------------------------------------------------------- 1 | {% load i18n wagtailadmin_tags %} 2 | {% if transcripts %} 3 | {% if is_searching %} 4 |

5 | {% blocktrans count counter=transcripts|length %} 6 | There is one match 7 | {% plural %} 8 | There are {{ counter }} matches 9 | {% endblocktrans %} 10 |

11 | 12 | {% search_other %} 13 | {% endif %} 14 | 15 | {% include "cast/transcript/list.html" %} 16 | 17 | {% include "wagtailadmin/shared/pagination_nav.html" with items=transcripts is_searching=is_searching linkurl="cast-transcript:index" %} 18 | {% else %} 19 | {% if is_searching %} 20 |

{% blocktrans %}Sorry, no transcript files match "{{ query_string }}"{% endblocktrans %}

21 | 22 | {% search_other %} 23 | {% else %} 24 | {% url 'cast-transcript:add' as casttranscript_add_transcript_url %} 25 | {% if current_collection %} 26 |

{% blocktrans %}You haven't uploaded any transcripts in this collection. You can upload transcript files.{% endblocktrans %}

27 | {% else %} 28 |

{% blocktrans %}You haven't uploaded any transcripts. You can upload transcript files.{% endblocktrans %}

29 | {% endif %} 30 | {% endif %} 31 | {% endif %} 32 | -------------------------------------------------------------------------------- /cast/templates/cast/twitter/card_player.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 46 | 47 | -------------------------------------------------------------------------------- /cast/templates/cast/video/add.html: -------------------------------------------------------------------------------- 1 | {% extends "wagtailadmin/base.html" %} 2 | {% load i18n %} 3 | {% load wagtailimages_tags %} 4 | {% block titletag %} 5 | {% trans "Add video" %} 6 | {% endblock %} 7 | 8 | {% block extra_js %} 9 | {{ block.super }} 10 | {{ form.media.js }} 11 | 12 | {% url 'wagtailadmin_tag_autocomplete' as autocomplete_url %} 13 | 20 | {% endblock %} 21 | 22 | {% block extra_css %} 23 | {{ block.super }} 24 | {{ form.media.css }} 25 | {% endblock %} 26 | 27 | {% block content %} 28 | {% trans "Add video" as add_str %} 29 | {% include "wagtailadmin/shared/header.html" with title=add_str icon="media" %} 30 | 31 |
32 |
33 | {% csrf_token %} 34 |
    35 | {% for field in form %} 36 |
  • 37 | {% include "wagtailadmin/shared/field.html" %} 38 |
  • 39 | {% endfor %} 40 |
  • 41 | 42 |
  • 43 |
44 |
45 |
46 | {% endblock %} 47 | -------------------------------------------------------------------------------- /cast/templates/cast/video/chooser.html: -------------------------------------------------------------------------------- 1 | {% extends "wagtailadmin/widgets/chooser.html" %} 2 | {% block chooser_class %}video-chooser{% endblock %} 3 | 4 | {% block chosen_state_view %} 5 | {{ title }} 6 | {% endblock %} 7 | 8 | {% block edit_chosen_item_url %}{{ edit_url }}{% endblock %} 9 | -------------------------------------------------------------------------------- /cast/templates/cast/video/chooser_results.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% if videos %} 3 | {% if is_searching %} 4 |

5 | {% blocktrans count counter=videos.paginator.count %} 6 | There is one match 7 | {% plural %} 8 | There are {{ counter }} matches 9 | {% endblocktrans %} 10 |

11 | {% else %} 12 |

{% trans "Latest video" %}

13 | {% endif %} 14 | 15 | {% include "cast/video/list.html" with choosing=1 %} 16 | 17 | {% include pagination_template with items=videos is_ajax=1 %} 18 | {% else %} 19 |

{% blocktrans %}Sorry, no media files match "{{ query_string }}"{% endblocktrans %}

20 | {% endif %} 21 | -------------------------------------------------------------------------------- /cast/templates/cast/video/confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "wagtailadmin/base.html" %} 2 | {% load wagtailadmin_tags i18n %} 3 | {% block titletag %}{% blocktrans with title=video.title %}Delete {{ title }}{% endblocktrans %}{% endblock %} 4 | {% block content %} 5 | {% trans "Delete video" as del_str %} 6 | {% include "wagtailadmin/shared/header.html" with title=del_str subtitle=video.title icon="media" %} 7 | 8 |
9 |

{% trans "Are you sure you want to delete this video?" %}

10 |
11 | {% csrf_token %} 12 | 13 |
14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /cast/templates/cast/video/results.html: -------------------------------------------------------------------------------- 1 | {% load i18n wagtailadmin_tags %} 2 | 3 | {% if videos %} 4 | {% if is_searching %} 5 |

6 | {% blocktrans count counter=videos|length %} 7 | There is one match 8 | {% plural %} 9 | There are {{ counter }} matches 10 | {% endblocktrans %} 11 |

12 | 13 | {% search_other %} 14 | {% endif %} 15 | 16 | {% include "cast/video/list.html" %} 17 | 18 | {% include "wagtailadmin/shared/pagination_nav.html" with items=videos is_searching=is_searching linkurl="castvideo:index" %} 19 | {% else %} 20 | {% if is_searching %} 21 |

{% blocktrans %}Sorry, no videos files match "{{ query_string }}"{% endblocktrans %}

22 | 23 | {% search_other %} 24 | {% else %} 25 | {% url 'castvideo:add_video' as castmedia_add_video_url %} 26 | {% if current_collection %} 27 |

{% blocktrans %}You haven't uploaded any videos in this collection. You can upload video files.{% endblocktrans %}

28 | {% else %} 29 |

{% blocktrans %}You haven't uploaded any videos. You can upload video files.{% endblocktrans %}

30 | {% endif %} 31 | {% endif %} 32 | {% endif %} 33 | -------------------------------------------------------------------------------- /cast/templates/cast/video/video.html: -------------------------------------------------------------------------------- 1 | {% if value.poster %} 2 | 6 | {% else %} 7 | 11 | {% endif %} 12 | -------------------------------------------------------------------------------- /cast/templates/cast/video_form.html: -------------------------------------------------------------------------------- 1 | {% extends "cast_base.html" %} 2 | {% load static %} 3 | 4 | {% block content %} 5 |
6 |
7 | {% csrf_token %} 8 | {{ form.as_p }} 9 |

10 |
11 | {% endblock content %} 12 | -------------------------------------------------------------------------------- /cast/templates/cast/wagtail/_file_field.html: -------------------------------------------------------------------------------- 1 | {% extends "wagtailadmin/shared/field.html" %} 2 | {% load i18n %} 3 | {% block form_field %} 4 | {{ media.filename }}

5 | {% trans "Change media file:" %} 6 | {{ field }} 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /cast/templates/cast/wagtail/_file_field_as_li.html: -------------------------------------------------------------------------------- 1 | {% load wagtailadmin_tags %} 2 |
  • 3 | {% include "cast/wagtail/_file_field.html" %} 4 |
  • 5 | -------------------------------------------------------------------------------- /cast/templates/cast/wagtail/_thumbnail_field.html: -------------------------------------------------------------------------------- 1 | {% extends "wagtailadmin/shared/field.html" %} 2 | {% load i18n %} 3 | {% block form_field %} 4 | 5 | {% if media.thumbnail %} 6 | {{ media.thumbnail_filename }}

    7 | {% endif %} 8 | 9 | {% trans "Change thumbnail file:" %} 10 | {{ field }} 11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /cast/templates/cast/wagtail/_thumbnail_field_as_li.html: -------------------------------------------------------------------------------- 1 | {% load wagtailadmin_tags %} 2 |
  • 3 | {% include "cast/wagtail/_thumbnail_field.html" %} 4 |
  • 5 | -------------------------------------------------------------------------------- /cast/templates/comments/comment.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | {% block comment_item %} 4 | {% if preview %}

    {% trans "Preview of your comment" %}

    {% endif %} 5 |
    6 | {% block comment_title %} 7 | {% if comment.url %}{% endif %} 8 | {% if comment.name %}{{ comment.name }}{% else %}{% trans "Anonymous" %}{% endif %}{% comment %} 9 | {% endcomment %}{% if comment.url %}{% endif %} 10 | {% blocktrans with submit_date=comment.submit_date %}on {{ submit_date }}{% endblocktrans %} 11 | {% if not comment.is_public %}({% trans "moderated" %}){% endif %} 12 | {% if USE_THREADEDCOMMENTS and not preview %}{% trans "reply" %}{% endif %} 13 | {% endblock %} 14 |
    15 | 16 |
    {{ comment.comment|linebreaks }}
    17 | {% endblock %} 18 | 19 | -------------------------------------------------------------------------------- /cast/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | from collections.abc import Iterable 3 | 4 | from django.core.files.storage import Storage 5 | 6 | 7 | def storage_walk_paths(storage: Storage, cur_dir: str = "") -> Iterable[str]: 8 | dirs, files = storage.listdir(cur_dir) 9 | for directory in dirs: 10 | new_dir = os.path.join(cur_dir, directory) 11 | for path in storage_walk_paths(storage, cur_dir=new_dir): 12 | yield path 13 | for file_name in files: 14 | path = os.path.join(cur_dir, file_name) 15 | yield path 16 | -------------------------------------------------------------------------------- /cast/views/__init__.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.http import HttpRequest 3 | 4 | from .htmx_helpers import HtmxHttpRequest 5 | 6 | 7 | class AuthenticatedHttpRequest(HttpRequest): 8 | user: User 9 | 10 | 11 | __all__ = [ 12 | "HtmxHttpRequest", 13 | "AuthenticatedHttpRequest", 14 | ] 15 | -------------------------------------------------------------------------------- /cast/views/htmx_helpers.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpRequest 2 | from django_htmx.middleware import HtmxDetails 3 | 4 | 5 | # Typing pattern recommended by django-stubs: 6 | # https://github.com/typeddjango/django-stubs#how-can-i-create-a-httprequest-thats-guaranteed-to-have-an-authenticated-user 7 | class HtmxHttpRequest(HttpRequest): 8 | cast_site_template_base_dir: str 9 | htmx: HtmxDetails 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /command_lines.txt: -------------------------------------------------------------------------------- 1 | # convert video from atem to format usable in firefox 2 | ffmpeg -i 2021-06-28_dinos.mp4 -codec copy 2021-06-28_dinos_new.mp4 3 | 4 | # run example server 5 | cd example 6 | python manage.py runserver --settings example_site.settings.dev 0.0:8000 7 | -------------------------------------------------------------------------------- /django-cast-example.ps1: -------------------------------------------------------------------------------- 1 | # to start the example don't forget to set the envs 2 | Set-Item "env:DJANGO_SETTINGS_MODULE" example_site.settings.dev 3 | python manage.py runserver localhost:8000 4 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/audio.rst: -------------------------------------------------------------------------------- 1 | .. _audio_overview: 2 | 3 | ***** 4 | Audio 5 | ***** 6 | 7 | You can upload audio files to the server and play them back in the browser. 8 | 9 | Audio Models 10 | ============ 11 | 12 | Audio files are represented by the `Audio` model. Audio files have a `title`, a 13 | `subtitle`, `tags` and four file fields pointing to the file in different audio formats: 14 | 15 | * `m4a` - AAC, works best on Apple/iOS devices 16 | * `mp3` - MP3, works everywhere, but has no time index so the whole file has to be downloaded before playback can start 17 | * `oga` - OGG Vorbis, maybe remove this one because apple is now adding support for opus, too 18 | * `opus` - Opus, better quality per bitrate than all other formats, but not as well known as the others 19 | 20 | Since podcast feeds only support one audio file per episode, there is one feed 21 | per audio format. The feeds are generated automatically and can be found at 22 | `feed/podcast//rss.xml`. 23 | 24 | Playback 25 | ======== 26 | 27 | For playback of audio content `Podlove Web Player `_ 28 | version 5 is used. 29 | 30 | .. Hint:: 31 | 32 | Currently supported features: 33 | 34 | * Chapter marks 35 | * Download button 36 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../AUTHORS.rst 2 | -------------------------------------------------------------------------------- /docs/backup.rst: -------------------------------------------------------------------------------- 1 | ###### 2 | Backup 3 | ###### 4 | 5 | *********************** 6 | Database Backup/Restore 7 | *********************** 8 | 9 | There's no unified way to backup and restore a database. For my personal projects 10 | I use ansible playbooks like this: 11 | 12 | * `backup `_ 13 | * `restore `_ 14 | 15 | It would be nice to be able to fetch all the relevant database contents by just 16 | reading from the cast REST-api and recreate the contents by just writing to 17 | another cast REST-api. This would make it possible to backup and restore really 18 | easy. But for now you have to do something database specific. 19 | 20 | Howto Restore a Database 21 | ======================== 22 | 23 | .. code-block:: shell 24 | 25 | python commands.py production-db-to-local 26 | cd backups 27 | mv 2023-09-18-22:17:27_homepage.sql.gz db.staging.psql.gz 28 | cd .. 29 | cd deploy 30 | ansible-playbook restore_database.yml --limit staging 31 | 32 | ******************** 33 | Media Backup/Restore 34 | ******************** 35 | 36 | Backup and restore are supported by the `media_backup` and `media_restore` 37 | :ref:`management commands `. 38 | 39 | Once we have a unified way to backup and restore the database, we can also 40 | integrate the media backup and restore into a single command. 41 | -------------------------------------------------------------------------------- /docs/comments.rst: -------------------------------------------------------------------------------- 1 | ******** 2 | Comments 3 | ******** 4 | 5 | You can enable / disable comments on app, blog and post-level. For app-level, 6 | there's a global switch you can use in the :ref:`settings `. 7 | Blog and post models have a comments_enabled database field. They are set 8 | to ``True`` by default. 9 | 10 | Caveats 11 | ======= 12 | 13 | The ajax-calls django-fluent-comments_ does depend on the availability of a 14 | full jquery version. 15 | 16 | .. _`django-fluent-comments`: https://github.com/django-fluent/django-fluent-comments 17 | 18 | Comment Spam Filter 19 | =================== 20 | 21 | There's a simple 22 | `Naive Bayes-based `_ 23 | spam filter for comments built in. It's not very smart, but it's good 24 | enough to filter out most spam. It's also very easy to train and very fast 25 | to run. And it's only slightly above one hundred lines of pure Python code. 26 | 27 | A comment is considered ham if it's public and not removed. All other comments 28 | are considered spam. It's possible to re-train the spam filter via a 29 | `Django Admin `_ 30 | action on the :code:`SpamFilter` model. The precision, recall and F1 performance 31 | indicators are also shown in the admin interface. 32 | 33 | .. image:: images/spam_filter_performance.png 34 | :width: 800 35 | :alt: Show a spam filter row in the Django admin interface 36 | -------------------------------------------------------------------------------- /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.45" 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 | -------------------------------------------------------------------------------- /docs/context-processors.rst: -------------------------------------------------------------------------------- 1 | ****************** 2 | Context Processors 3 | ****************** 4 | 5 | ======================= 6 | Template base Directory 7 | ======================= 8 | 9 | There are probably more pages in your site that are not a blog 10 | or a post or otherwise related to wagtail. How do you use the same 11 | base template for all of them? And how do you switch the base 12 | template for those pages automatically when you switch the 13 | base template / theme for your site in wagtail? 14 | 15 | The answer is to use a `context processor `_. 16 | It will add two variables to the context of every template in your site: 17 | 18 | - ``cast_base_template``: the base template to use for the current theme 19 | - ``cast_site_template_base_dir``: the raw template base directory 20 | for the current theme holding ``bootstrap4`` or ``plain`` for example 21 | 22 | The ``cast_base_template`` variable is the one you could use in 23 | your local template to extend the base template: 24 | 25 | .. code-block:: html+django 26 | 27 | {% extends cast_base_template %} 28 | ... 29 | 30 | If you want to use this context processor, add it to your 31 | ``settings.py``: 32 | 33 | .. code-block:: python 34 | 35 | TEMPLATES = [ 36 | { 37 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 38 | 'DIRS': [], 39 | 'APP_DIRS': True, 40 | 'OPTIONS': { 41 | 'context_processors': [ 42 | ... 43 | 'cast.context_processors.site_template_base_dir', 44 | ], 45 | }, 46 | }, 47 | ] 48 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /docs/django-admin.rst: -------------------------------------------------------------------------------- 1 | ************ 2 | Django-Admin 3 | ************ 4 | 5 | The file sizes of an audio object are cached automatically. But 6 | for old audio objects there's an admin action where you can update 7 | the file size cache for all associated audio files. 8 | 9 | .. image:: images/cache_file_sizes_admin_action.png 10 | :width: 800 11 | :alt: Show the admin action to update the file size cache 12 | -------------------------------------------------------------------------------- /docs/episode.rst: -------------------------------------------------------------------------------- 1 | ******* 2 | Episode 3 | ******* 4 | 5 | Podcast episodes are Wagtail pages that have a :ref:`podcast ` 6 | page as a parent. They have the same features as blog :ref:`posts `, 7 | but with some additional fields for better podcast support. 8 | 9 | Cover Image 10 | =========== 11 | 12 | In addition to the effect setting a cover image for a post has, setting a 13 | cover image for an episode will also be used as the episode's artwork in the 14 | podcast feed and in the Podlove Web Player. 15 | 16 | If no cover image is set for the episode, the :ref:`blog `’s cover 17 | image will be used. 18 | 19 | Podcast Audio 20 | ============= 21 | 22 | The `podcast_audio` field is required for an episode. It is used for the 23 | enclosure tag in the podcast feed. 24 | 25 | Promote > Title 26 | =============== 27 | 28 | This will be used as the title of the episode in the Podlove Web Player. 29 | 30 | Promote > Description 31 | ===================== 32 | 33 | This will be used as the description of the episode in the Podlove Web Player. 34 | 35 | Keywords 36 | ======== 37 | 38 | Keywords are set in the podcast feed as the iTunes keywords tag. 39 | 40 | Explicit 41 | ======== 42 | 43 | Explicit content is set in the podcast feed as the iTunes explicit tag. 44 | 45 | Block 46 | ===== 47 | 48 | Indicates whether the episode is blocked from iTunes or not. 49 | -------------------------------------------------------------------------------- /docs/features.rst: -------------------------------------------------------------------------------- 1 | ######## 2 | Features 3 | ######## 4 | 5 | .. include:: responsive-images.rst 6 | .. include:: frontend.rst 7 | .. include:: django-admin.rst 8 | .. include:: social-media.rst 9 | .. include:: comments.rst 10 | .. include:: blog.rst 11 | .. include:: post.rst 12 | .. include:: podcast.rst 13 | .. include:: episode.rst 14 | .. include:: tags.rst 15 | .. include:: image.rst 16 | .. include:: gallery.rst 17 | .. include:: video.rst 18 | .. include:: audio.rst 19 | .. include:: transcript.rst 20 | .. include:: themes.rst 21 | .. include:: management-commands.rst 22 | -------------------------------------------------------------------------------- /docs/frontend.rst: -------------------------------------------------------------------------------- 1 | ******** 2 | Frontend 3 | ******** 4 | 5 | Pagination 6 | ========== 7 | 8 | The blog index page comes with pagination support. You can set the 9 | number of posts per page using the `POST_LIST_PAGINATION` setting. 10 | 11 | If there are more then 3 pages, there will be a "..." in the pagination. 12 | If there are more then 10 pages, there will be two "..." in the pagination. 13 | -------------------------------------------------------------------------------- /docs/gallery.rst: -------------------------------------------------------------------------------- 1 | .. _gallery_overview: 2 | 3 | ******* 4 | Gallery 5 | ******* 6 | 7 | Galleries are a collection of images. They are used to display a 8 | list of thumbnails and a larger view of the selected image. See 9 | :ref:`responsive images ` for more 10 | information about thumbnails. 11 | -------------------------------------------------------------------------------- /docs/howto/first-cast.rst: -------------------------------------------------------------------------------- 1 | *************************** 2 | Your first django-cast site 3 | *************************** 4 | 5 | Just to see whether the link works 😄? 6 | -------------------------------------------------------------------------------- /docs/howto/integrate-cast.rst: -------------------------------------------------------------------------------- 1 | ****************************** 2 | Integrate with an Existing App 3 | ****************************** 4 | 5 | Just to see whether the link works 😄? 6 | -------------------------------------------------------------------------------- /docs/image.rst: -------------------------------------------------------------------------------- 1 | .. _image_overview: 2 | 3 | ***** 4 | Image 5 | ***** 6 | 7 | Images are mostly just the normal Wagtail Images. But they are 8 | rendered using a `picture` tag supporting `srcset` and `sizes` attributes 9 | for :ref:`responsive images `. 10 | -------------------------------------------------------------------------------- /docs/images/blog_template_base_dir_setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ephes/django-cast/fa9f6a4e9f0ce3ecc1517f5032994e031ecc03a6/docs/images/blog_template_base_dir_setting.png -------------------------------------------------------------------------------- /docs/images/cache_file_sizes_admin_action.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ephes/django-cast/fa9f6a4e9f0ce3ecc1517f5032994e031ecc03a6/docs/images/cache_file_sizes_admin_action.png -------------------------------------------------------------------------------- /docs/images/spam_filter_performance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ephes/django-cast/fa9f6a4e9f0ce3ecc1517f5032994e031ecc03a6/docs/images/spam_filter_performance.png -------------------------------------------------------------------------------- /docs/images/template_base_dir_setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ephes/django-cast/fa9f6a4e9f0ce3ecc1517f5032994e031ecc03a6/docs/images/template_base_dir_setting.png -------------------------------------------------------------------------------- /docs/images/twitter_card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ephes/django-cast/fa9f6a4e9f0ce3ecc1517f5032994e031ecc03a6/docs/images/twitter_card.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ########### 2 | Django Cast 3 | ########### 4 | 5 | Welcome to the Django Cast documentation! Django Cast is a blogging and podcasting 6 | package for `Django `_ based on `Wagtail `_. 7 | 8 | This documentation is still in development and there are a lot of blanks to 9 | fill since Django Cast switched to Wagtail. 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | :caption: Contents: 14 | 15 | installation 16 | contributing 17 | features 18 | tests 19 | backup 20 | howto/index 21 | release 22 | releases/index 23 | models 24 | context-processors 25 | settings 26 | authors 27 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/management-commands.rst: -------------------------------------------------------------------------------- 1 | ********************* 2 | Django-Admin Commands 3 | ********************* 4 | 5 | .. _cast_management_commands: 6 | 7 | There are some management-commands bundled with django-cast. Most of them 8 | are dealing with the management of media files. 9 | 10 | * ``recalc_video_posters``: Recalculate the poster images for all videos. 11 | * ``media_backup``: Backup media files from production to backup storage backend (requires Django >= 4.2). 12 | * ``media_sizes``: Print the sizes of all media files stored in the production storage backend (requires Django >= 4.2). 13 | * ``media_replace``: Replace files on production storage backend with versions from local file system (requires Django >= 4.2). 14 | This might be useful for videos for which you now have a better compressed 15 | version, but you don't want to generate a new name. 16 | * ``media_restore``: Restore media files from backup storage backend to production storage backend (requires Django >= 4.2). 17 | * ``media_stale``: Print the paths of all media files stored in the production storage backend that are not 18 | referenced in the database (requires Django >= 4.2). 19 | * ``sync_renditions``: Create missing renditions for images and remove obsolete ones. Useful if you have changed 20 | the rendition sizes in your settings or added new image formats. 21 | -------------------------------------------------------------------------------- /docs/models.rst: -------------------------------------------------------------------------------- 1 | ###### 2 | Models 3 | ###### 4 | 5 | 6 | Some reference documentation about how the models work. 7 | 8 | **** 9 | Post 10 | **** 11 | 12 | A post is a single blog post. It's the parent of episodes, too. 13 | 14 | Template Logic 15 | ============== 16 | 17 | Since you can set a base directory for templates, the `get_template` 18 | method is overridden to get the base directory from the request and 19 | return the correct template. 20 | 21 | To be able to render the description of a post without the base template, 22 | there's a `_local_template_name` attribute set on the `Post` class that 23 | can be used to override the template name. This is used for example in 24 | the `get_description` method to render the description of the post using 25 | the `post_body.html` template for the feed and the twitter card. 26 | 27 | API-Fields 28 | ========== 29 | 30 | There are some additional fields that can be fetched from the wagtail pages API: 31 | * uuid - a unique identifier for the post 32 | * visible_date - the date the post is visible, usually used for sorting 33 | * comments_enabled - whether comments are enabled for this post 34 | * body - the body stream field of the post 35 | * html_overview - the rendered html of the overview section of the body (used in SPA themes) 36 | * html_detail - the rendered html of the overview and detail section of the body (used in SPA themes) 37 | 38 | ******* 39 | Episode 40 | ******* 41 | 42 | A special kind of post that has some additional fields and logic. 43 | -------------------------------------------------------------------------------- /docs/podcast.rst: -------------------------------------------------------------------------------- 1 | .. _podcast_overview: 2 | 3 | ******* 4 | Podcast 5 | ******* 6 | 7 | Podcasts are :ref:`blogs ` that have some additional features to 8 | better support podcasting. 9 | 10 | There are some additional fields that are available for podcasts: 11 | 12 | Itunes Artwork 13 | ============== 14 | The image that will be used in the podcast feed as the iTunes artwork. 15 | -------------------------------------------------------------------------------- /docs/release.rst: -------------------------------------------------------------------------------- 1 | *************** 2 | Release Process 3 | *************** 4 | 5 | Bump Version Number 6 | ------------------- 7 | 8 | Change the version number in following files: 9 | 10 | - cast/__init__.py 11 | - docs/conf.py 12 | - pyproject.toml 13 | - README.md 14 | 15 | Javascript 16 | ---------- 17 | Update dependencies: 18 | 19 | .. code-block:: shell 20 | 21 | $ cd javascript 22 | $ npm outdated 23 | $ npm update 24 | 25 | Build the Javascript (image-gallery component): 26 | 27 | .. code-block:: shell 28 | 29 | $ cd javascript 30 | $ npx vite build 31 | $ cd dist/ 32 | $ mv .vite/manifest.json manifest.json 33 | $ rm -r .vite 34 | $ rm ../../cast/static/cast/vite/* 35 | $ cp * ../../cast/static/cast/vite/ 36 | 37 | 38 | Test Python Versions and Merge develop into main 39 | ------------------------------------------------ 40 | 41 | Make sure all tests are passing on supported Python versions: 42 | 43 | .. code-block:: bash 44 | 45 | $ tox 46 | 47 | Merge the develop branch into the main branch: 48 | 49 | .. code-block:: bash 50 | 51 | $ git checkout main 52 | $ git pull && git merge develop 53 | $ git push 54 | 55 | Create the Release on GitHub 56 | ---------------------------- 57 | 58 | 1. Create a new tag on GitHub 59 | 2. Copy the release notes from the previous version and change them accordingly 60 | 3. Mark as pre-release 61 | 62 | Build the Release Wheels and Publish to PyPI 63 | -------------------------------------------- 64 | 65 | Create the package: 66 | 67 | .. code-block:: bash 68 | 69 | $ uv build 70 | $ uv publish --token your-token 71 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.0.rst: -------------------------------------------------------------------------------- 1 | 0.1.0 (2018-11-05) 2 | ------------------ 3 | 4 | * First release on PyPI. 5 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.1.rst: -------------------------------------------------------------------------------- 1 | 0.1.1 (2018-11-07) 2 | ------------------ 3 | 4 | * Travis build is ok. 5 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.10.rst: -------------------------------------------------------------------------------- 1 | 0.1.10 (2019-03-21) 2 | ------------------- 3 | 4 | * Dont limit the number of items in feed (was 5 items) 5 | * Workaround for ogg files (ending differs for Audio model field name) 6 | * Added opus format to Audio model 7 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.11.rst: -------------------------------------------------------------------------------- 1 | 0.1.11 (2019-03-21) 2 | ------------------- 3 | 4 | * Fixed requirements for package 5 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.12.rst: -------------------------------------------------------------------------------- 1 | 0.1.12 (2019-03-22) 2 | ------------------- 3 | 4 | * Improved installation documentation 5 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.13.rst: -------------------------------------------------------------------------------- 1 | 0.1.13 (2019-03-22) 2 | ------------------- 3 | 4 | * Release to update read the docs 5 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.14.rst: -------------------------------------------------------------------------------- 1 | 0.1.14 (2019-03-23) 2 | ------------------- 3 | 4 | * Added rtfd configuration file to be able to use python 3 :/ 5 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.15.rst: -------------------------------------------------------------------------------- 1 | 0.1.15 (2019-03-23) 2 | ------------------- 3 | 4 | * Trying again... rtfd still failing 5 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.16.rst: -------------------------------------------------------------------------------- 1 | 0.1.16 (2019-03-23) 2 | ------------------- 3 | 4 | * Finally, rtfd is working again, including screencast 5 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.17.rst: -------------------------------------------------------------------------------- 1 | 0.1.17 (2019-04-15) 2 | ------------------- 3 | 4 | * Added chaptermarks feature 5 | * Duration is now displayed correctly in podlove player 6 | * If an audio upload succeeded, add the uploaded element to podcast audio select form 7 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.18.rst: -------------------------------------------------------------------------------- 1 | 0.1.18 (2019-04-18) 2 | ------------------- 3 | 4 | * Fixed broken update view due to empty chaptermarks + test 5 | * Fixed two image/video javascript bugs 6 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.19.rst: -------------------------------------------------------------------------------- 1 | 0.1.19 (2019-04-24) 2 | ------------------- 3 | 4 | * Added fulltext search 5 | * Added filtering by date + some faceted navigation support 6 | * use overwritable template block for feeds section (could be used for podlove subscribe button) 7 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.2.rst: -------------------------------------------------------------------------------- 1 | 0.1.2 (2018-11-08) 2 | ------------------ 3 | 4 | * Added some requirements 5 | * Release Documentation 6 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.20.rst: -------------------------------------------------------------------------------- 1 | 0.1.20 (2019-04-24) 2 | ------------------- 3 | 4 | * Fixed version history 5 | * Better release docs 6 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.21.rst: -------------------------------------------------------------------------------- 1 | 0.1.21 (2019-04-24) 2 | ------------------- 3 | 4 | * Fixed package dependencies 5 | * Better release docs 6 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.22.rst: -------------------------------------------------------------------------------- 1 | 0.1.22 (2019-04-28) 2 | ------------------- 3 | 4 | * Use proper time field for chaptermark start instead of char 5 | * Improved test coverage 6 | * Improved video dimension handling for handbrake generated portrait videos 7 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.23.rst: -------------------------------------------------------------------------------- 1 | 0.1.23 (2019-05-16) 2 | ------------------- 3 | 4 | * Comment en/disabling per site/blog/post 5 | * Fix duration extraction and small issues with the installation docs @jnns 6 | * Support for comments by @oryon-dominik 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.25.rst: -------------------------------------------------------------------------------- 1 | 0.1.25 (2019-05-23) 2 | ------------------- 3 | 4 | * Bugfix: i18n should now work, finally 5 | * Bugfix: Allow empty chaptermarks text field + test 6 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.26.rst: -------------------------------------------------------------------------------- 1 | 0.1.26 (2019-05-23) 2 | ------------------- 3 | 4 | * Bugfix: i18n should now work, finally!!1 duh 5 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.27.rst: -------------------------------------------------------------------------------- 1 | 0.1.27 (2019-05-27) 2 | ------------------- 3 | 4 | * Extended documentation 5 | * It's now possible to mark content as "for post detail page" only 6 | * Changed documentation to work with comments 7 | * Fixed comments dependencies in setup.py 8 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.28.rst: -------------------------------------------------------------------------------- 1 | 0.1.28 (2019-06-03) 2 | ------------------- 3 | 4 | * Added some analytics support: import your access.log and view dashboard with hits/day,week 5 | * Fixed pub_date bug, leading to safari not being able to update posts + some tests 6 | * Use local web-player and subscribe button (didn't improve performance, though :( ) 7 | * Fixed detail content not included in feed (shownotes were missing) bug 8 | * Added some deployment documentation for heroku, ec2 and docker 9 | * Overwritable block for detail link in post list template + documentation 10 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.29.rst: -------------------------------------------------------------------------------- 1 | 0.1.29 (2020-01-03) 2 | ------------------- 3 | 4 | * Use poetry instead of requirements.txt and setup.py 5 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.3.rst: -------------------------------------------------------------------------------- 1 | 0.1.3 (2018-11-17) 2 | ------------------ 3 | 4 | * Fixed css/static icons 5 | * Merged pull request from SmartC2016 to fix javascript block issue 6 | * Added some documentation 7 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.30.rst: -------------------------------------------------------------------------------- 1 | 0.1.30 (2020-10-25) 2 | ------------------- 3 | 4 | * fixed some logfile parsing bug 5 | * tried to add wagtail but removed it again 6 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.31.rst: -------------------------------------------------------------------------------- 1 | 0.1.31 (never released) 2 | ----------------------- 3 | 4 | * Trying to use vuepress for documentation 5 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.32.rst: -------------------------------------------------------------------------------- 1 | 0.1.32 (never released) 2 | ----------------------- 3 | 4 | * not clear 5 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.33.rst: -------------------------------------------------------------------------------- 1 | 0.1.33 (never released) 2 | ----------------------- 3 | 4 | * dont know anymore 5 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.34.rst: -------------------------------------------------------------------------------- 1 | 0.1.34 (never released) 2 | ----------------------- 3 | 4 | * dont know anymore 5 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.35.rst: -------------------------------------------------------------------------------- 1 | 0.1.35 (2020-10-25) 2 | ------------------- 3 | 4 | * last package released that is based on `master` branch 5 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.4.rst: -------------------------------------------------------------------------------- 1 | 0.1.4 (2018-11-18) 2 | ------------------ 3 | 4 | * Include css via cast_base.html 5 | * audio fixes 6 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.5.rst: -------------------------------------------------------------------------------- 1 | 0.1.5 (2018-11-21) 2 | ------------------ 3 | 4 | * basic feed support (rss/atom) for podcasts 5 | * travis now runs tests with ffprobe, too 6 | * documentation fixes from @SmartC2016 and @oryon-dominik 7 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.6.rst: -------------------------------------------------------------------------------- 1 | 0.1.6 (2019-02-28) 2 | ------------------ 3 | 4 | * Use filepond for media uploads (images video) 5 | * Improved portrait video support 6 | * Get api prefix programatically from schema 7 | * Fixed link to podcast in itunes (was feed, now it's post list) 8 | * Set visible date to now if it's not set 9 | * use load static instead of staticfiles (deprecated) 10 | * Fixed language displayed in itunes (you have to set it in base.py in settings) 11 | * Dont try to be fancy, just display a plain list of feed on top of post list site (and podcast feeds only if blog.is_podcast is True) 12 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.7.rst: -------------------------------------------------------------------------------- 1 | 0.1.7 (2019-02-28) 2 | ------------------ 3 | 4 | * forgot linting 5 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.8.rst: -------------------------------------------------------------------------------- 1 | 0.1.8 (2019-02-28) 2 | ------------------ 3 | 4 | * Added support for m4v and improved dimension detection for iOS videos 5 | * Added some tests for different video sources 6 | -------------------------------------------------------------------------------- /docs/releases/0.1/0.1.9.rst: -------------------------------------------------------------------------------- 1 | 0.1.9 (2019-03-12) 2 | ------------------ 3 | 4 | * Added some podcast specific fields to post edit form 5 | * If two audio uploads have the same name, add them to the same model instance 6 | * Added audio file support for post edit form 7 | * Show which audio files already were uploaded 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/releases/0.2.1.rst: -------------------------------------------------------------------------------- 1 | 0.2.1 (2023-01-06) 2 | ------------------ 3 | 4 | Fixed a bad spam filter bug leading to comments being classified as ham which have lots of unknown words. 5 | 6 | * fixed comment posting (min instead of slim jquery dependency) 7 | * api overview is working again 8 | -------------------------------------------------------------------------------- /docs/releases/0.2.10.rst: -------------------------------------------------------------------------------- 1 | 0.2.10 (2023-03-26) 2 | ------------------- 3 | 4 | Filter fixes + htmx for pagination. 5 | 6 | * #88 Fixed date facet is removed from form submit 7 | * Moved app settings init into `appsettings.py` 8 | * Provided some default values for settings which don't have 9 | to be in the settings file 10 | * Convenience imports for urlpatterns 11 | * #85 Added htmx support for pagination in plain and 12 | sbootstrap4 themes 13 | -------------------------------------------------------------------------------- /docs/releases/0.2.11.rst: -------------------------------------------------------------------------------- 1 | 0.2.11 (2023-04-03) 2 | ------------------- 3 | 4 | Documentation improvements 5 | 6 | * #74 Added some documentation for django-admin commands and fixed ``s3_stale`` 7 | * Transferred some old documentation 8 | * Fixed language in an image example 9 | -------------------------------------------------------------------------------- /docs/releases/0.2.12.rst: -------------------------------------------------------------------------------- 1 | 0.2.12 (2023-04-10) 2 | ------------------- 3 | 4 | Extended test-matrix, better `podcast_audio` field handling 5 | 6 | * Extended the tox test-matrix to include Django 4.0 7 | * #90 moved the `podcast_audio` field and added a warning 8 | -------------------------------------------------------------------------------- /docs/releases/0.2.13.rst: -------------------------------------------------------------------------------- 1 | 0.2.13 (2023-04-17) 2 | ------------------- 3 | 4 | Fixing tests for Django 4.0 5 | 6 | * Fixed a mypy error and added one test for htmx.target is None 7 | * #91 fixed tests for Django 4.0 8 | * Added support badge for Django 4.0 and 4.1 in README 9 | * Updated pre-commit hooks 10 | * #90 added a validation for `episode.podcast_audio` to be set when an episode is published 11 | * #83 make it possible to set custom templates for http error responses 12 | -------------------------------------------------------------------------------- /docs/releases/0.2.14.rst: -------------------------------------------------------------------------------- 1 | 0.2.14 (2023-05-01) 2 | ------------------- 3 | 4 | Wagtail 5 compatibility. 5 | 6 | * removed `nb_black` dev dependency 7 | * Support for Wagtail 5 8 | * removed `max-height` from `.cast-image` for bootstrap4-theme because of contortion 9 | -------------------------------------------------------------------------------- /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.16.rst: -------------------------------------------------------------------------------- 1 | 0.2.16 (2023-06-04) 2 | ------------------- 3 | 4 | Finished support for SPA themes (comments and podlove player). 5 | 6 | * Made comments data available via wagtail api #95 7 | * Made some data available just via page attribute, because it's only added via template and script tag #95 8 | -------------------------------------------------------------------------------- /docs/releases/0.2.17.rst: -------------------------------------------------------------------------------- 1 | 0.2.17 (2023-08-07) 2 | ------------------- 3 | 4 | Improved plain theme. 5 | 6 | * Better pagination styling for plain theme. 7 | * Update pre-commit hooks 8 | * Include wagtail 5.1 in test matrix (tox.ini) 9 | * Fixed the "land on last page by default" wagtail admin pagination bug #98 10 | * Fixed the broken chooser buttons for audio and video (wagtail 5.1, action-chooser class was removed from button) #98 11 | * Fixed duplicate chooser button for audio and video #98 12 | * Added some documentation for audio content 13 | -------------------------------------------------------------------------------- /docs/releases/0.2.18.rst: -------------------------------------------------------------------------------- 1 | 0.2.18 (2023-08-14) 2 | ------------------- 3 | 4 | Use the STORAGES setting introduced in Django 4.2 to improve media backup and restore #99. 5 | -------------------------------------------------------------------------------- /docs/releases/0.2.19.rst: -------------------------------------------------------------------------------- 1 | 0.2.19 (2023-08-21) 2 | ------------------- 3 | 4 | Added tags and categories for posts. I planned for long adding a feature 5 | like this, but avoided implementing it because I didn't understand what I 6 | did in `filters.py`. It's a beta feature and I probably will keep only 7 | categories or tags, but not both. 8 | 9 | - Updated some documentation in top level README.md 10 | - Added tags / categories to posts #100 😅 11 | - Temporarily deactivated the mypy pre-commit hook because mypy 1.5.0 is not compatible with django-stubs 12 | - Had to drop support for Wagtail 3 because of conflicting migrations (taggit) 13 | -------------------------------------------------------------------------------- /docs/releases/0.2.2.rst: -------------------------------------------------------------------------------- 1 | 0.2.2 (2023-01-15) 2 | ------------------ 3 | 4 | Podlove Player share sheet and code blocks for Wagtail. 5 | 6 | * Fixed share sheet of Podlove Player 7 | * Added code blocks for Wagtail 8 | * Refactoring removing some of the @pytest.mark.django_db decorators 9 | * Removed all the request analytics stuff (switched using plausible...) 10 | * Removed django-watson dependency since it is not used anymore 11 | -------------------------------------------------------------------------------- /docs/releases/0.2.20.rst: -------------------------------------------------------------------------------- 1 | 0.2.20 (2023-08-28) 2 | ------------------- 3 | 4 | Smaller fixes and improvements around tags, categories and filters. 5 | 6 | - Fixed the .readthedocs.yml #102 7 | - Moved facet filter related logic into the facet filter classes, this is a lot cleaner #100 8 | - Remove empty facet filter fields from filterset form #100 9 | - Refactored the post filter classes to be more consistent #100 10 | - Category and tags are now available for podcast episodes, too #100 11 | - Facet counts for tags and categories are now available via the API #100 12 | -------------------------------------------------------------------------------- /docs/releases/0.2.21.rst: -------------------------------------------------------------------------------- 1 | 0.2.21 (2023-09-04) 2 | ------------------- 3 | 4 | Make it possible to select a new theme on the website and store 5 | the selection result in the session. 6 | 7 | - Added a snippet viewset to be able to do CRUD on tags via wagtail admin #100 8 | - Let users select a theme for the site and store the choice in the session #105 9 | - Added a new api endpoint to get the list of available themes and update the selected theme #105 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/releases/0.2.23.rst: -------------------------------------------------------------------------------- 1 | 0.2.23 (2023-10-21) 2 | ------------------- 3 | 4 | Added Python 3.12 support, dropped Python 3.9 support. 5 | 6 | - Workaround for missing comment form error messages #107 (thanks @petermeissner) 7 | - Python Version support (+3.12 -3.9) #106 8 | - A little bit more documentation for database restore #56 9 | -------------------------------------------------------------------------------- /docs/releases/0.2.24.rst: -------------------------------------------------------------------------------- 1 | 0.2.24 (2023-11-25) 2 | ------------------- 3 | 4 | Responsive images using the `picture` element and some smaller fixes. 5 | 6 | - Make title link to detail and strip dash from date link in bootstrap4 7 | template thanks @neingeist 8 | - Re-added the `o` ordering parameter to make it possible to order by 9 | visible date 10 | - Upgraded Python Version for GitHub Actions to 3.12 11 | - #108 Fixed test coverage for `cast/filters.py` 12 | - #109 Responsive images revisited 13 | - picture element and avif format for images 14 | - images now have an `alt` attribute 15 | - using `srcset` in combination with `sizes` attributes for smaller images 16 | - removed `wagtail_srcset` dependency 17 | - #110 Fixed mypy errors by putting a workaround in place 18 | -------------------------------------------------------------------------------- /docs/releases/0.2.25.rst: -------------------------------------------------------------------------------- 1 | 0.2.25 (2023-12-10) 2 | ------------------- 3 | 4 | Use the original image if no renditions are generated. Includes now a 5 | management command to sync renditions. Revisited how the renditions are 6 | passed to the template. 7 | 8 | - #112 If no renditions are generated, use the original image as the default rendition 9 | - #113 Management command to sync renditions 10 | - Changed tox.ini to include Django 5.0 and removed Django 4.0 11 | -------------------------------------------------------------------------------- /docs/releases/0.2.26.rst: -------------------------------------------------------------------------------- 1 | 0.2.26 (2023-12-31) 2 | ------------------- 3 | 4 | Use a web component for the image gallery. Have a proper build pipeline 5 | for the web components Javascript and some tests. Have a working htmx layout 6 | for the gallery. It's still a bit slower compared to the web component, but 7 | it's a start. 8 | 9 | - #114 Use web component for image gallery 10 | - Some cleanup regarding javascript paths etc. - using a global cast prefix for example 11 | - #116 Gallery layout using htmx for bootstrap4 and 5 themes 12 | -------------------------------------------------------------------------------- /docs/releases/0.2.27.rst: -------------------------------------------------------------------------------- 1 | 0.2.27 (2024-01-02) 2 | ------------------- 3 | 4 | Bugfix release. Multiple galleries per post were not working when using the 5 | default gallery layout. 6 | 7 | - #117 Fix multiple galleries per post 8 | -------------------------------------------------------------------------------- /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/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.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 | -------------------------------------------------------------------------------- /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.31.rst: -------------------------------------------------------------------------------- 1 | 0.2.31 (2024-05-05) 2 | ------------------- 3 | 4 | Podlove web player 5 is now used by default. Version 4 will still continue 5 | to work, but it'is not officially supported anymore. This is important because adding a 6 | transcript feature depends on this player version. It is now also possible to 7 | overwrite the template that is used for an audio block in a theme. This is useful 8 | if you want to heavily customize the appearance of the player. 9 | 10 | - #65 Use podlove web player 5 11 | - #132 Create the feed repository from cachable data, too 12 | - #133 Test for each post whether it is a podcast episode 13 | - #135 Support for wagtail 6.1 14 | - #137 Make `audio.html` template themeable via `template_base_dir` 15 | - #138 Small mypy fix 16 | -------------------------------------------------------------------------------- /docs/releases/0.2.32.rst: -------------------------------------------------------------------------------- 1 | 0.2.32 (2024-05-12) 2 | ------------------- 3 | 4 | Added an optional cover image to post. For podcasts the iTunes artwork will be used 5 | as a fallback. 6 | 7 | - #56 Added some documentation for post and related models 8 | - #136 Added cover image to post 9 | - #139 Fixed codecov upload 10 | -------------------------------------------------------------------------------- /docs/releases/0.2.33.rst: -------------------------------------------------------------------------------- 1 | 0.2.33 (2024-05-26) 2 | ------------------- 3 | 4 | Some fixes. 5 | 6 | - #141 Fixed rendering the API version of an episode 7 | - #142 Fixed internal server error on post to facet counts list endpoint 8 | - #143 Fixed serving posts from blog A in feed of blog B 9 | - #144 Updated htmx to 2.0.0 beta 4 10 | - #145 Pin wagtail to < 6.1 because of broken audio and video choosers 11 | - #146 Fixed missing get_cover_image method on Post model -> audio player now works again for non-Episodes 12 | -------------------------------------------------------------------------------- /docs/releases/0.2.34.rst: -------------------------------------------------------------------------------- 1 | 0.2.34 (2024-06-16) 2 | ------------------- 3 | 4 | Cover image for Blogs do also make sense. Removed the 5 | pin to `Wagtail 6.1` since the audio and video choosers 6 | are working again. 7 | 8 | - #136 Added cover image for Blog, too 9 | - #145 Surprisingly simple fix for the audio and video choosers in Wagtail 6.1 10 | - #147 Fixed podlovePlayer not working after htmx pagination 11 | - #148 Fixed remove null bytes in api request query parameters 12 | - #149 Removed some unused dependencies 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/releases/0.2.36.rst: -------------------------------------------------------------------------------- 1 | 0.2.36 (2024-07-07) 2 | ------------------- 3 | 4 | Bugfixes and new subtitle field for blog and podcast. 5 | 6 | - #140 Fixed the `page_ptr` stuff again. This time for real. 7 | - #157 Fixed the `get_prep_value` getting called with weird values again. This time for real. 8 | - #134 Added subtitle to blog / podcast 9 | - #136 Added some more documentation for `cover_image` and `cover_image_alt` 10 | - #158 Fixed a no reverse match error when trying to render the podlove player during preview 11 | -------------------------------------------------------------------------------- /docs/releases/0.2.37.rst: -------------------------------------------------------------------------------- 1 | 0.2.37 (2024-07-20) 2 | ------------------- 3 | 4 | Bugfixes and new subtitle field for blog and podcast. 5 | 6 | - #159 Fixed author name in post detail page header for bs4 and base templates 7 | - #160 Squash migrations 8 | -------------------------------------------------------------------------------- /docs/releases/0.2.38.rst: -------------------------------------------------------------------------------- 1 | 0.2.38 (2024-08-04) 2 | ------------------- 3 | 4 | Support for Wagtail 6.2. Had to deactivate the mypy pre-commit hook because of 5 | it no longer being able to infer types of model fields. 6 | 7 | - #162 return a default template base dir name ("does_not_exist") instead of raising an exception when there are no wagtail sites 8 | -------------------------------------------------------------------------------- /docs/releases/0.2.39.rst: -------------------------------------------------------------------------------- 1 | 0.2.39 (2024-10-13) 2 | ------------------- 3 | 4 | Support for Python 3.13 has been added. Support for Python 3.10 has been dropped. 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/releases/0.2.40.rst: -------------------------------------------------------------------------------- 1 | 0.2.40 (2024-11-03) 2 | ------------------- 3 | Support for Wagtail 6.3 4 | 5 | Improved the performance of the web player by 6 | - #167 Waiting for the load event before initializing the player (instead of using DOMContentLoaded) 7 | - #167 Only initializing the player once the player is visible in the viewport 8 | - #167 Save space for the player in the layout, even if it's not visible 9 | - #167 New podlove player web component 10 | -------------------------------------------------------------------------------- /docs/releases/0.2.41.rst: -------------------------------------------------------------------------------- 1 | 0.2.41 (2025-01-05) 2 | ------------------- 3 | 4 | Started adding transcripts to the audio model. This is a work in progress and 5 | not yet finished. Switched from `flit` to `uv` because I started to use `uv` 6 | for all new projects and now gradually switch old ones over. 7 | 8 | - #167 Fixed the styling of the audio player 9 | - #168 Add transcript model 10 | - Better icons for audio, video and transcript MenuItems 11 | - Order the audio models by default by -created for forms with audio foreign key like TranscriptForm 12 | - #172 Switch from flit to uv 13 | - #173 Mypy fixes 14 | - #161 Release process order 15 | - Updated Javascript dependencies 16 | -------------------------------------------------------------------------------- /docs/releases/0.2.42.rst: -------------------------------------------------------------------------------- 1 | 0.2.42 (2025-02-15) 2 | ------------------- 3 | 4 | Wagtail 6.4 compatibility. You might to have to add something like that to your 5 | Django settings to make sure that the fulltext search works: 6 | 7 | .. code-block:: python 8 | 9 | TASKS = { 10 | "default": { 11 | "BACKEND": "django_tasks.backends.immediate.ImmediateBackend", 12 | "ENQUEUE_ON_COMMIT": False, 13 | } 14 | } 15 | 16 | - #175 Wagtail 6.4 compatibility 17 | - #176 Fix tox.ini use of uv 18 | - #176 Fix pyproject.toml dev dependencies 19 | - #176 Fix github actions 20 | - #168 Tests for transcripts -> coverage back at 100% 21 | -------------------------------------------------------------------------------- /docs/releases/0.2.43.rst: -------------------------------------------------------------------------------- 1 | 0.2.43 (2025-02-23) 2 | ------------------- 3 | 4 | Include transcript urls in the podcast feed. Some transcript fixes 5 | and preparations for the upcoming transcript html detail view. 6 | 7 | - Tested with Django 5.2 pre-release -> all tests worked 8 | - #168 include transcript urls in the podcast feed 9 | - #168 FIX: do not render podlove-player in the feed 10 | - #168 use absolute urls for podcast:transcript elements 11 | - #168 There's a rudimentary html view for the transcript 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/releases/0.2.45.rst: -------------------------------------------------------------------------------- 1 | 0.2.45 (2025-05-12) 2 | ------------------- 3 | 4 | Just official support for Wagtail 7 and Django 5.2. 5 | 6 | - #185 Wagtail 7 support 7 | -------------------------------------------------------------------------------- /docs/releases/0.2.5.rst: -------------------------------------------------------------------------------- 1 | 0.2.5 (2023-02-13) 2 | ------------------ 3 | 4 | Coverage reached 100%! 5 | 6 | * Some documentation enhancements/fixes 7 | * Wagtail 4.2 compatibility 8 | * Nearly complete type annotations 9 | * Added a few more tests -> 100% coverage 🥳 10 | * Moved coverage config into pyproject.toml 11 | * Fixed pagination #66 12 | -------------------------------------------------------------------------------- /docs/releases/0.2.6.rst: -------------------------------------------------------------------------------- 1 | 0.2.6 (2023-02-20) 2 | ------------------ 3 | 4 | Compatibility with django-crispy-forms >= 2 5 | 6 | * Added badge for supported Python versions 7 | * Added some pagination documentation 8 | * Added bootstrap4 template pack + settings 9 | * Fixed date facets breaking out of their container 10 | -------------------------------------------------------------------------------- /docs/releases/0.2.7.rst: -------------------------------------------------------------------------------- 1 | 0.2.7 (2023-03-05) 2 | ------------------ 3 | 4 | Pages API and small fixes. 5 | 6 | * Query optimization - didn't work, this needs some more infrastructure fist 7 | * Added a `pages` endpoint to the API 8 | * Added an `images` endpoint to the API 9 | * Added a `noindex` option to the Blog model to exclude a blog from search engines 10 | * Fixed audio web player name `embed.js` 11 | * Fixed a bug caused by mypy and static protocols raising a NotImplementedError in production :( 12 | -------------------------------------------------------------------------------- /docs/releases/0.2.8.rst: -------------------------------------------------------------------------------- 1 | 0.2.8 (2023-03-13) 2 | ------------------ 3 | 4 | Exchangeable templates. 5 | 6 | * Added the ability to exchange templates by using different template base directories 7 | * Fixed backlink from comment to post when not using Javascript 8 | -------------------------------------------------------------------------------- /docs/releases/0.2.9.rst: -------------------------------------------------------------------------------- 1 | 0.2.9 (2023-03-20) 2 | ------------------ 3 | 4 | Fixes for theme support and documentation. 5 | 6 | * Added support for themes via settings 7 | * New documentation section for settings 8 | * New documentation section for themes 9 | * Fixed a bug in the filter logic 10 | * New installation instructions (not yet complete) 11 | -------------------------------------------------------------------------------- /docs/releases/index.rst: -------------------------------------------------------------------------------- 1 | ******** 2 | Releases 3 | ******** 4 | 5 | Versions 6 | ======== 7 | 8 | .. toctree:: 9 | :maxdepth: 1 10 | 11 | 0.2.45 12 | 0.2.44 13 | 0.2.43 14 | 0.2.42 15 | 0.2.41 16 | 0.2.40 17 | 0.2.39 18 | 0.2.38 19 | 0.2.37 20 | 0.2.36 21 | 0.2.35 22 | 0.2.34 23 | 0.2.33 24 | 0.2.32 25 | 0.2.31 26 | 0.2.30 27 | 0.2.29 28 | 0.2.28 29 | 0.2.27 30 | 0.2.26 31 | 0.2.25 32 | 0.2.24 33 | 0.2.23 34 | 0.2.22 35 | 0.2.21 36 | 0.2.20 37 | 0.2.19 38 | 0.2.18 39 | 0.2.17 40 | 0.2.16 41 | 0.2.15 42 | 0.2.14 43 | 0.2.13 44 | 0.2.12 45 | 0.2.11 46 | 0.2.10 47 | 0.2.9 48 | 0.2.8 49 | 0.2.7 50 | 0.2.6 51 | 0.2.5 52 | 0.2.4 53 | 0.2.3 54 | 0.2.2 55 | 0.2.1 56 | 0.2.0 57 | 0.1/0.1.35 58 | 0.1/0.1.34 59 | 0.1/0.1.33 60 | 0.1/0.1.32 61 | 0.1/0.1.31 62 | 0.1/0.1.30 63 | 0.1/0.1.29 64 | 0.1/0.1.28 65 | 0.1/0.1.27 66 | 0.1/0.1.26 67 | 0.1/0.1.25 68 | 0.1/0.1.24 69 | 0.1/0.1.23 70 | 0.1/0.1.22 71 | 0.1/0.1.21 72 | 0.1/0.1.20 73 | 0.1/0.1.19 74 | 0.1/0.1.18 75 | 0.1/0.1.17 76 | 0.1/0.1.16 77 | 0.1/0.1.15 78 | 0.1/0.1.14 79 | 0.1/0.1.13 80 | 0.1/0.1.12 81 | 0.1/0.1.11 82 | 0.1/0.1.10 83 | 0.1/0.1.9 84 | 0.1/0.1.8 85 | 0.1/0.1.7 86 | 0.1/0.1.6 87 | 0.1/0.1.5 88 | 0.1/0.1.4 89 | 0.1/0.1.3 90 | 0.1/0.1.2 91 | 0.1/0.1.1 92 | 0.1/0.1.0 93 | 94 | Versioning Policy 95 | ================= 96 | 97 | To be defined 😅. 98 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | django 2 | furo 3 | sphinx 4 | -------------------------------------------------------------------------------- /docs/responsive-images.rst: -------------------------------------------------------------------------------- 1 | .. _response_images_overview: 2 | 3 | ***************** 4 | Responsive Images 5 | ***************** 6 | 7 | Responsive images adapt to different screen sizes, ensuring optimal 8 | display across devices. This project uses two types of responsive images: 9 | 10 | - **Normal Images:** Displayed within page content. 11 | - **Thumbnail Images:** Shown as thumbnails in content and enlarged in a modal upon clicking. 12 | 13 | **Image Renditions:** 14 | For each image, three renditions are created in AVIF and JPEG formats: 15 | - **1x Width:** Standard size, used in the `src` attribute of the `img` tag. 16 | - **2x Width:** Double size, used provided the image is not larger or nearly the same size as the original. 17 | - **3x Width:** Triple size, used only when necessary. 18 | These renditions are specified in the `srcset` attribute of the `source` or `img` elements. 19 | 20 | Those three renditions are put into the `srcset` attribute of the related `source` or `img` 21 | elements. 22 | 23 | Normal Images 24 | ============= 25 | 26 | Normal images fill a slot with a width of 1110px and a maximum height of 740px. 27 | But you can configure those values in the :ref:`settings `. 28 | 29 | Therefore, if you have an image which is quadratic and has a width of 3000px it 30 | will rendered with a maximum width of 740px, but delivered with a width of 2220px 31 | to high pixel density devices. 32 | 33 | Both AVIF and JPEG formats are supported. 34 | 35 | Thumbnail Images 36 | ================ 37 | 38 | Thumbnail images have a width of 120px and a maximum height of 80px. But you can 39 | also set those values in the :ref:`settings `. 40 | 41 | They also support AVIF and JPEG formats. 42 | -------------------------------------------------------------------------------- /docs/social-media.rst: -------------------------------------------------------------------------------- 1 | ************ 2 | Social Media 3 | ************ 4 | 5 | Twitter 6 | ======= 7 | 8 | If you share a link to a detail page of an episode, there will 9 | be a `Twitter Player Card `_ 10 | added to your tweet. At the moment you have to generate the 11 | metadata manually. For an example have a look at 12 | `this episode template `_. 13 | 14 | .. image:: images/twitter_card.png 15 | :width: 400 16 | :alt: Image of a Twitter Card 17 | -------------------------------------------------------------------------------- /docs/tags.rst: -------------------------------------------------------------------------------- 1 | ***************** 2 | Categories / Tags 3 | ***************** 4 | 5 | This is a beta feature. It is not yet fully implemented. Since I don't 6 | know yet if I will go with tags or categories, I added both and wait 7 | which one sticks 😄. 8 | 9 | Categories 10 | ========== 11 | 12 | Categories are one way to group posts. They come with their own snippet 13 | model so you can add them via the admin interface by clicking on one 14 | of the categories. A blog post can have multiple categories and a category 15 | can have multiple blog posts. If you want to add a new category, you have 16 | to add it using the wagtail admin interface. 17 | 18 | Categories might be the right thing if you do not have too many of 19 | them and they rarely change. 20 | 21 | 22 | Tags 23 | ==== 24 | 25 | Tags are another way to group posts. They come with their own link to 26 | the `taggit` tag model. You can add tags to a blog post by using the 27 | standard wagtail `tag` interface. A blog post can have multiple tags 28 | and a tag can have multiple blog posts. If you want to add a new tag, 29 | there's a text field with auto completion in the wagtail admin interface. 30 | 31 | Tags might be the right thing if you have a lot of them and they change 32 | often and you don't mind having to type them in the admin interface. 33 | -------------------------------------------------------------------------------- /docs/tests.rst: -------------------------------------------------------------------------------- 1 | ***************** 2 | Running the Tests 3 | ***************** 4 | 5 | Python 6 | ====== 7 | 8 | Start by checking out the source code of the project and switching to the development branch: 9 | 10 | .. code-block:: bash 11 | 12 | $ git clone git@github.com:ephes/django-cast.git 13 | $ cd django-cast 14 | $ git checkout develop 15 | 16 | Then, create a virtualenv and install the project dependencies: 17 | 18 | .. code-block:: bash 19 | 20 | $ uv venv 21 | $ uv pip install -e .[dev] 22 | 23 | Now create the test database: 24 | 25 | .. code-block:: bash 26 | 27 | $ uv run manage.py migrate 28 | 29 | The tests are then run by executing the following command: 30 | 31 | .. code-block:: bash 32 | 33 | $ uv run pytest 34 | 35 | You can measure the test coverage by running the following command: 36 | 37 | .. code-block:: bash 38 | 39 | $ uv run coverage run -m pytest && uv run coverage html && open htmlcov/index.html 40 | 41 | In order to make running tests faster, the test database is set up 42 | to be reused and migrations are not applied. This implies that if you 43 | have added a new Django migration to your codebase, you will need to 44 | execute the following commands below to re-create the test database: 45 | 46 | .. code-block:: bash 47 | 48 | $ rm tests/test_database.sqlite3 # delete the old test database 49 | $ uv run manage.py migrate # re-create the test database 50 | 51 | After that, you should be able to run the tests again. 52 | 53 | Javascript 54 | ========== 55 | 56 | .. code-block:: bash 57 | 58 | $ cd javascript 59 | $ npx vitest run 60 | -------------------------------------------------------------------------------- /docs/transcript.rst: -------------------------------------------------------------------------------- 1 | .. _transcript_overview: 2 | 3 | *********** 4 | Transcripts 5 | *********** 6 | 7 | You can upload transcript files to the server and display them 8 | alongside the audio player. They will be also included in the feed 9 | and can be used by podcast clients to display the transcript while 10 | listening to the episode. 11 | 12 | Transcript Models 13 | ================= 14 | 15 | Transcript files are represented by the `Transcript` model. Transcripts have an 16 | audio file they belong to, a `podlove` field that contains the transcript in 17 | the form that the `Podlove Web Player `_ 18 | can use. And two other file formats that are used for to be referenced in the 19 | feed: 20 | 21 | * `vtt` - WebVTT, a subtitle format in plain text 22 | * `dote` - DOTE, a json transcript format 23 | -------------------------------------------------------------------------------- /docs/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 | -------------------------------------------------------------------------------- /example/example_site/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ephes/django-cast/fa9f6a4e9f0ce3ecc1517f5032994e031ecc03a6/example/example_site/__init__.py -------------------------------------------------------------------------------- /example/example_site/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ephes/django-cast/fa9f6a4e9f0ce3ecc1517f5032994e031ecc03a6/example/example_site/settings/__init__.py -------------------------------------------------------------------------------- /example/example_site/settings/dev.py: -------------------------------------------------------------------------------- 1 | from .base import * # noqa 2 | 3 | 4 | # SECURITY WARNING: don't run with debug turned on in production! 5 | DEBUG = True 6 | 7 | # SECURITY WARNING: keep the secret key used in production secret! 8 | SECRET_KEY = "fi6y_c!w#4+16srq_%z+(dj=7d8&5+reik+_171*=e8(0(157x" 9 | 10 | # SECURITY WARNING: define the correct hosts in production! 11 | ALLOWED_HOSTS = ["*"] 12 | 13 | EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" 14 | 15 | INSTALLED_APPS.extend( # noqa 16 | [ 17 | "django_extensions", 18 | ] 19 | ) 20 | 21 | 22 | try: 23 | from .local import * # noqa 24 | except ImportError: 25 | pass 26 | -------------------------------------------------------------------------------- /example/example_site/settings/production.py: -------------------------------------------------------------------------------- 1 | from .base import * # noqa 2 | 3 | 4 | DEBUG = False 5 | 6 | try: 7 | from .local import * # noqa 8 | except ImportError: 9 | pass 10 | 11 | 12 | ADMIN_URL = "hidden_admin/" 13 | -------------------------------------------------------------------------------- /example/example_site/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block body_class %}template-404{% endblock %} 4 | 5 | {% block content %} 6 |

    Page not found

    7 | 8 |

    Sorry, this page could not be found.

    9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /example/example_site/templates/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Internal server error 6 | 7 | 8 | 9 |

    Internal server error

    10 | 11 |

    Sorry, there seems to be an error. Please try again soon.

    12 | 13 | 14 | -------------------------------------------------------------------------------- /example/example_site/templates/pages/about.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 |

    4 | Ein bisschen About Text. 5 |

    6 | {% endblock content %} 7 | -------------------------------------------------------------------------------- /example/example_site/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for example_site project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | 15 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_site.settings.dev") 16 | 17 | application = get_wsgi_application() 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /example/staticfiles/css/project.css: -------------------------------------------------------------------------------- 1 | /* These styles are generated from project.scss. */ 2 | 3 | .alert-debug { 4 | color: black; 5 | background-color: white; 6 | border-color: #d6e9c6; 7 | } 8 | 9 | .alert-error { 10 | color: #b94a48; 11 | background-color: #f2dede; 12 | border-color: #eed3d7; 13 | } 14 | -------------------------------------------------------------------------------- /example/staticfiles/js/project.js: -------------------------------------------------------------------------------- 1 | /* Project specific Javascript goes here. */ 2 | const $ = window.$ 3 | $('.form-group').removeClass('row') 4 | -------------------------------------------------------------------------------- /javascript/.gitignore: -------------------------------------------------------------------------------- 1 | # bundles 2 | dist/ 3 | 4 | # node_modules 5 | node_modules/ 6 | -------------------------------------------------------------------------------- /javascript/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'jsdom', 4 | moduleFileExtensions: [ 5 | 'js', 6 | 'json', 7 | 'ts' 8 | ], 9 | transform: { 10 | '^.+\\.ts$': 'ts-jest' 11 | }, 12 | setupFiles: [ 13 | '/tests/unit/setup.ts' 14 | ], 15 | globals: { 16 | 'ts-jest': { 17 | tsconfig: 'tsconfig.json' 18 | } 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /javascript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "vite": "^6" 4 | }, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "test": "vitest -r js/src/tests/", 9 | "coverage": "vitest run --coverage", 10 | "preview": "vite preview" 11 | }, 12 | "devDependencies": { 13 | "@testing-library/jest-dom": "^6", 14 | "@vue/test-utils": "^2", 15 | "jsdom": "^26", 16 | "vitest": "^3" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /javascript/src/tests/image-gallery-bs4.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test, describe } from "vitest"; 2 | import ImageGallery from "@/gallery/image-gallery-bs4"; 3 | 4 | 5 | describe("image gallery test", () => { 6 | test("gallery test current image is null", () => { 7 | const gallery = new ImageGallery() 8 | console.log("gallery: ", gallery) 9 | expect(gallery.currentImage).toBe(null) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /javascript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "useDefineForClassFields": true, 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "types": ["vite/client", "@types/jest", "@types/node"], 13 | "lib": ["esnext", "dom"], 14 | "paths": { 15 | "@/*": ["./src/js/*"] 16 | } 17 | }, 18 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx"] 19 | } 20 | -------------------------------------------------------------------------------- /javascript/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from "path"; 2 | import { defineConfig } from "vite"; 3 | import type { UserConfig as VitestUserConfigInterface } from "vitest/config" 4 | 5 | const vitestConfig: VitestUserConfigInterface = { 6 | test: { 7 | globals: true, 8 | environment: "jsdom", 9 | }, 10 | } 11 | 12 | export default defineConfig({ 13 | plugins: [], 14 | test: vitestConfig.test, 15 | root: resolve("./"), 16 | base: "/static/", 17 | server: { 18 | host: "0.0.0.0", 19 | port: 5173, 20 | open: false, 21 | watch: { 22 | usePolling: true, 23 | disableGlobbing: false, 24 | }, 25 | }, 26 | resolve: { 27 | extensions: [".js", ".json", ".ts"], 28 | alias: { 29 | "@": resolve("./src/"), 30 | }, 31 | }, 32 | build: { 33 | outDir: resolve("./dist"), 34 | assetsDir: "", 35 | manifest: true, 36 | emptyOutDir: true, 37 | target: "es2015", 38 | rollupOptions: { 39 | input: { 40 | main: resolve("./src/gallery/image-gallery-bs4.ts"), 41 | podlovePlayer: resolve("./src/audio/podlove-player.ts") 42 | }, 43 | output: { 44 | chunkFileNames: undefined, 45 | }, 46 | }, 47 | }, 48 | }) 49 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | 6 | 7 | if __name__ == "__main__": 8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") 9 | from django.core.management import execute_from_command_line 10 | 11 | execute_from_command_line(sys.argv) 12 | -------------------------------------------------------------------------------- /notebooks/feed/remove_timestamped_model.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "d28c3426", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "post = Post.objects.first()" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 2, 16 | "id": "6abb4a0b", 17 | "metadata": {}, 18 | "outputs": [ 19 | { 20 | "data": { 21 | "text/plain": [ 22 | "datetime.datetime(2021, 9, 26, 7, 18, 14, 113295, tzinfo=datetime.timezone.utc)" 23 | ] 24 | }, 25 | "execution_count": 2, 26 | "metadata": {}, 27 | "output_type": "execute_result" 28 | } 29 | ], 30 | "source": [ 31 | "post.last_published_at" 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": null, 37 | "id": "f4e6a3b0", 38 | "metadata": {}, 39 | "outputs": [], 40 | "source": [ 41 | "post.la" 42 | ] 43 | } 44 | ], 45 | "metadata": { 46 | "kernelspec": { 47 | "display_name": "Django Shell-Plus", 48 | "language": "python", 49 | "name": "django_extensions" 50 | }, 51 | "language_info": { 52 | "codemirror_mode": { 53 | "name": "ipython", 54 | "version": 3 55 | }, 56 | "file_extension": ".py", 57 | "mimetype": "text/x-python", 58 | "name": "python", 59 | "nbconvert_exporter": "python", 60 | "pygments_lexer": "ipython3", 61 | "version": "3.11.1" 62 | } 63 | }, 64 | "nbformat": 4, 65 | "nbformat_minor": 5 66 | } 67 | -------------------------------------------------------------------------------- /notebooks/moderation/example_site: -------------------------------------------------------------------------------- 1 | ../../example/example_site -------------------------------------------------------------------------------- /notebooks/twitter/player_card.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "60ead15b", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "episode = Episode.objects.first()" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 3, 16 | "id": "6aadd7b5", 17 | "metadata": {}, 18 | "outputs": [ 19 | { 20 | "data": { 21 | "text/plain": [ 22 | "" 23 | ] 24 | }, 25 | "execution_count": 3, 26 | "metadata": {}, 27 | "output_type": "execute_result" 28 | } 29 | ], 30 | "source": [ 31 | "episode.blog" 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": null, 37 | "id": "8a607ea3", 38 | "metadata": {}, 39 | "outputs": [], 40 | "source": [] 41 | } 42 | ], 43 | "metadata": { 44 | "kernelspec": { 45 | "display_name": "Django Shell-Plus", 46 | "language": "python", 47 | "name": "django_extensions" 48 | }, 49 | "language_info": { 50 | "codemirror_mode": { 51 | "name": "ipython", 52 | "version": 3 53 | }, 54 | "file_extension": ".py", 55 | "mimetype": "text/x-python", 56 | "name": "python", 57 | "nbconvert_exporter": "python", 58 | "pygments_lexer": "ipython3", 59 | "version": "3.11.1" 60 | } 61 | }, 62 | "nbformat": 4, 63 | "nbformat_minor": 5 64 | } 65 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ephes/django-cast/fa9f6a4e9f0ce3ecc1517f5032994e031ecc03a6/tests/__init__.py -------------------------------------------------------------------------------- /tests/apps_test.py: -------------------------------------------------------------------------------- 1 | from django.apps import apps 2 | 3 | 4 | def test_delete_wagtail_images_app_setting(mocker): 5 | cast_config = apps.get_app_config("cast") 6 | 7 | # delete images True -> do not disconnect post_delete_file_cleanup 8 | mocker.patch("cast.appsettings.DELETE_WAGTAIL_IMAGES", True) 9 | mocked_disconnect = mocker.patch("cast.appsettings.post_delete.disconnect") 10 | cast_config.ready() 11 | assert mocked_disconnect.call_count == 0 12 | 13 | # delete images False -> disconnect post_delete_file_cleanup 14 | mocker.patch("cast.appsettings.DELETE_WAGTAIL_IMAGES", False) 15 | mocked_disconnect = mocker.patch("cast.appsettings.post_delete.disconnect") 16 | cast_config.ready() 17 | assert mocked_disconnect.call_count == 1 18 | -------------------------------------------------------------------------------- /tests/cast/static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ephes/django-cast/fa9f6a4e9f0ce3ecc1517f5032994e031ecc03a6/tests/cast/static/.gitkeep -------------------------------------------------------------------------------- /tests/convenience_test.py: -------------------------------------------------------------------------------- 1 | from cast.cast_and_wagtail_urls import urlpatterns 2 | 3 | 4 | def test_convenience_urls(): 5 | assert len(urlpatterns) == 4 6 | -------------------------------------------------------------------------------- /tests/episode_detail_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | class TestTwitterPlayerCard: 5 | pytestmark = pytest.mark.django_db 6 | 7 | def test_includes_card_with_podcast_audio(self, client, episode): 8 | episode = episode 9 | detail_url = episode.get_url() 10 | 11 | r = client.get(detail_url) 12 | assert r.status_code == 200 13 | 14 | content = r.content.decode("utf-8") 15 | assert "html" in content 16 | assert episode.title in content 17 | assert "Twitter Player Card" in content 18 | -------------------------------------------------------------------------------- /tests/fixtures/access.log: -------------------------------------------------------------------------------- 1 | 79.230.47.221 - - [01/Dec/2018:06:55:44 +0100] "GET /show/ HTTP/2.0" 200 2678 2 | 79.230.47.221 - - [01/Dec/2018:06:55:44 +0100] "GET /show/ HTTP/2.0" 200 2678 3 | 79.230.47.221 - - [01/Dec/2018:06:55:44 +0100] "GET /show/ HTTP/2.0" 200 2678 4 | 79.230.47.221 - - [01/Dec/2018:06:55:44 +0100] "GET /show/ HTTP/2.0" 200 2678 5 | 79.230.47.221 - - [01/Dec/2018:06:55:44 +0100] "GET /show/ HTTP/2.0" 200 2678 6 | 5.9.225.241 - - [02/Dec/2018:19:08:37 +0100] "GET /show/ HTTP/1.1" 200 2846 "-" "http.rb/3.2.0 (Mastodon/2.4.5; +https://toot.berlin/)" 7 | -------------------------------------------------------------------------------- /tests/fixtures/test.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ephes/django-cast/fa9f6a4e9f0ce3ecc1517f5032994e031ecc03a6/tests/fixtures/test.m4a -------------------------------------------------------------------------------- /tests/fixtures/test.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ephes/django-cast/fa9f6a4e9f0ce3ecc1517f5032994e031ecc03a6/tests/fixtures/test.mp4 -------------------------------------------------------------------------------- /tests/fixtures/test_video.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ephes/django-cast/fa9f6a4e9f0ce3ecc1517f5032994e031ecc03a6/tests/fixtures/test_video.mp4 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/meta_views_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.urls import reverse 3 | 4 | 5 | @pytest.mark.django_db 6 | def test_twitter_player(client, episode): 7 | episode = episode 8 | url = reverse("cast:twitter-player", kwargs={"episode_slug": episode.slug, "blog_slug": episode.blog.slug}) 9 | r = client.get(url) 10 | assert r.status_code == 200 11 | 12 | content = r.content.decode("utf-8") 13 | assert str(episode.uuid) in content 14 | assert "embed.5.js" in content 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/snippets_test.py: -------------------------------------------------------------------------------- 1 | from cast.models import PostCategory 2 | 3 | 4 | def test_post_category_name(): 5 | category = PostCategory(name="Test Category", slug="test-category") 6 | assert str(category) == "Test Category" 7 | -------------------------------------------------------------------------------- /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("fluent_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 | -------------------------------------------------------------------------------- /tests/utils_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.core.files.storage import default_storage 3 | 4 | from cast.utils import storage_walk_paths 5 | 6 | 7 | class TestUtils: 8 | @pytest.mark.django_db 9 | def test_walk_fs_paths(self, client, episode): 10 | audio_path = episode.podcast_audio.m4a.path 11 | found = False 12 | for path in storage_walk_paths(default_storage): 13 | if audio_path.endswith(path): 14 | found = True 15 | assert found 16 | -------------------------------------------------------------------------------- /tests/video_upload_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.urls import reverse 3 | 4 | 5 | class TestVideoUpload: 6 | @pytest.mark.django_db 7 | def test_upload_video_not_authenticated(self, client, minimal_mp4): 8 | upload_url = reverse("cast:api:upload_video") 9 | 10 | minimal_mp4.seek(0) 11 | r = client.post(upload_url, {"original": minimal_mp4}) 12 | # redirect to login 13 | assert r.status_code == 302 14 | 15 | @pytest.mark.django_db 16 | def test_upload_video_authenticated(self, client, user, minimal_mp4): 17 | # login 18 | r = client.login(username=user.username, password=user._password) 19 | 20 | self.called_create_poster = False 21 | 22 | def set_called_create_poster(): 23 | self.called_create_poster = True 24 | 25 | # mock create poster? 26 | # Video._saved_create_poster = Video._create_poster 27 | # Video._create_poster = lambda x: set_called_create_poster() 28 | 29 | # upload 30 | upload_url = reverse("cast:api:upload_video") 31 | minimal_mp4.seek(0) 32 | r = client.post(upload_url, {"original": minimal_mp4}) 33 | 34 | # unmock 35 | # Video._create_poster = Video._saved_create_poster 36 | 37 | assert r.status_code == 201 38 | assert int(r.content.decode("utf-8")) > 0 39 | 40 | # check mocked function has been called - no longer necessary since we use 41 | # a real mp4 now. 42 | # assert self.called_create_poster 43 | -------------------------------------------------------------------------------- /tests/wagtail_image_views_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from cast.models.image_renditions import create_missing_renditions_for_posts 4 | 5 | 6 | class TestPostWithImageDetail: 7 | pytestmark = pytest.mark.django_db 8 | 9 | def test_get_post_with_image_detail(self, client, post_with_image): 10 | post = post_with_image 11 | create_missing_renditions_for_posts([post]) 12 | detail_url = post.get_url() 13 | 14 | r = client.get(detail_url) 15 | assert r.status_code == 200 16 | 17 | content = r.content.decode("utf-8") 18 | assert "html" in content 19 | 20 | # make sure css for image included in rendered image block 21 | assert "cast-image" in content 22 | -------------------------------------------------------------------------------- /tests/widget_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from cast.widgets import AdminAudioChooser, AdminVideoChooser 4 | 5 | 6 | @pytest.mark.django_db 7 | def test_video_chooser_get_value_data_value_not_none(video): 8 | avc = AdminVideoChooser() 9 | 10 | # get video data by passing the primary key 11 | data = avc.get_value_data(video.pk) 12 | assert data["id"] == video.pk 13 | 14 | # get video data by passing the video object 15 | data = avc.get_value_data(video) 16 | assert data["id"] == video.pk 17 | 18 | 19 | def test_video_chooser_render_js_init(): 20 | avc = AdminVideoChooser() 21 | js = avc.render_js_init(1, "name", None) 22 | assert js == "createVideoChooser(1);" 23 | 24 | 25 | @pytest.mark.django_db 26 | def test_audio_chooser_get_value_data_value_not_none(audio): 27 | avc = AdminAudioChooser() 28 | 29 | # get audio data by passing the primary key 30 | data = avc.get_value_data(audio.pk) 31 | assert data["id"] == audio.pk 32 | 33 | # get audio data by passing the audio object 34 | data = avc.get_value_data(audio) 35 | assert data["id"] == audio.pk 36 | 37 | 38 | def test_audio_chooser_render_js_init(): 39 | avc = AdminAudioChooser() 40 | js = avc.render_js_init(1, "name", None) 41 | assert js == "createAudioChooser(1);" 42 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{311,312,313}-django{42,52}-wagtail6 4 | py{311,312,313}-django{42,52}-wagtail7 5 | cleanup 6 | 7 | isolated_build = true 8 | 9 | [testenv] 10 | deps = 11 | django42: Django>=4.2,<5 12 | django52: Django>=5.0,<6 13 | wagtail6: wagtail>=6,<7 14 | wagtail7: wagtail>=7,<8 15 | 16 | setenv = DJANGO_SETTINGS_MODULE=tests.settings 17 | commands = 18 | uv pip install -e . 19 | uv run python manage.py migrate 20 | uv run python -m pytest 21 | 22 | allowlist_externals = 23 | uv 24 | [testenv:cleanup] 25 | skip_install = true 26 | isolated_build = false 27 | commands = 28 | rm tests/test_database.sqlite3 29 | sh -c "uv run python manage.py migrate > /dev/null 2>&1" 30 | echo "All tests done!" 31 | 32 | allowlist_externals = 33 | rm 34 | sh 35 | echo 36 | 37 | [testenv:pre-commit] 38 | deps = pre-commit 39 | commands = pre-commit run --all-files 40 | --------------------------------------------------------------------------------