diff options
Diffstat (limited to 'app/podcasts')
-rw-r--r-- | app/podcasts/admin.py | 10 | ||||
-rw-r--r-- | app/podcasts/feeds.py | 229 | ||||
-rw-r--r-- | app/podcasts/migrations/0001_initial.py | 60 | ||||
-rw-r--r-- | app/podcasts/migrations/__init__.py | 0 | ||||
-rw-r--r-- | app/podcasts/models.py | 132 | ||||
-rw-r--r-- | app/podcasts/templates/podcasts/detail.html | 108 | ||||
-rw-r--r-- | app/podcasts/templates/podcasts/list.html | 31 | ||||
-rw-r--r-- | app/podcasts/urls.py | 19 | ||||
-rw-r--r-- | app/podcasts/urls_feeds.py | 17 | ||||
-rw-r--r-- | app/podcasts/views.py | 29 |
10 files changed, 635 insertions, 0 deletions
diff --git a/app/podcasts/admin.py b/app/podcasts/admin.py new file mode 100644 index 0000000..b6adfc6 --- /dev/null +++ b/app/podcasts/admin.py @@ -0,0 +1,10 @@ +from django.contrib import admin +from .models import Podcast + +@admin.register(Podcast) +class PodcastAdmin(admin.ModelAdmin): + list_display = ('title',) + search_fields = ['title', 'body_markdown'] + + class Media: + js = ('next-prev-links.js',) diff --git a/app/podcasts/feeds.py b/app/podcasts/feeds.py new file mode 100644 index 0000000..24d47bd --- /dev/null +++ b/app/podcasts/feeds.py @@ -0,0 +1,229 @@ +import datetime +from podcasts.models import Episode, Podcast +from django.contrib.syndication.views import Feed +from django.contrib.sites.shortcuts import get_current_site +from django.views.generic.base import RedirectView +from django.utils.feedgenerator import rfc2822_date, Rss201rev2Feed, Atom1Feed +from django.shortcuts import get_object_or_404 + +from .models import MIME_CHOICES + +class ITunesElements(object): + + def add_root_elements(self, handler): + """ Add additional elements to the podcast object""" + super(ITunesElements, self).add_root_elements(handler) + + podcast = self.feed["podcast"] + + if podcast.featured_image: + # grab thumbs here + #itunes_sm_url = thumbnailer.get_thumbnail(aliases["itunes_sm"]).url + #itunes_lg_url = thumbnailer.get_thumbnail(aliases["itunes_lg"]).url + if itunes_sm_url and itunes_lg_url: + handler.addQuickElement("itunes:image", attrs={"href": itunes_lg_url}) + handler.startElement("image", {}) + handler.addQuickElement("url", itunes_sm_url) + handler.addQuickElement("title", self.feed["title"]) + handler.addQuickElement("link", self.feed["link"]) + handler.endElement("image") + + handler.addQuickElement("guid", str(podcast.uuid), attrs={"isPermaLink": "false"}) + handler.addQuickElement("itunes:subtitle", self.feed["subtitle"]) + handler.addQuickElement("itunes:author", podcast.publisher) + handler.startElement("itunes:owner", {}) + handler.addQuickElement("itunes:name", podcast.publisher) + handler.addQuickElement("itunes:email", podcast.publisher_email) + handler.endElement("itunes:owner") + handler.addQuickElement("itunes:category", attrs={"text": self.feed["categories"][0]}) + handler.addQuickElement("itunes:summary", podcast.description) + handler.addQuickElement("itunes:explicit", "no") + handler.addQuickElement("keywords", podcast.keywords) + try: + handler.addQuickElement("lastBuildDate", + rfc2822_date(podcast.episode_set.filter(status=1)[1].pub_date)) + except IndexError: + pass + handler.addQuickElement("generator", "Luxagraf's Django Web Framework") + handler.addQuickElement("docs", "http://blogs.law.harvard.edu/tech/rss") + + def add_item_elements(self, handler, item): + """ Add additional elements to the episode object""" + super(ITunesElements, self).add_item_elements(handler, item) + + podcast = item["podcast"] + episode = item["episode"] + if episode.featured_image: + #grab episode thumbs + #itunes_sm_url = None + #itunes_lg_url = None + if itunes_sm_url and itunes_lg_url: + handler.addQuickElement("itunes:image", attrs={"href": itunes_lg_url}) + handler.startElement("image", {}) + handler.addQuickElement("url", itunes_sm_url) + handler.addQuickElement("title", episode.title) + handler.addQuickElement("link", episode.get_absolute_url()) + handler.endElement("image") + + handler.addQuickElement("guid", str(episode.uuid), attrs={"isPermaLink": "false"}) + handler.addQuickElement("copyright", "{0} {1}".format(podcast.license, + datetime.date.today().year)) + handler.addQuickElement("itunes:author", episode.podcast.publisher) + handler.addQuickElement("itunes:subtitle", episode.subtitle) + handler.addQuickElement("itunes:summary", episode.description) + handler.addQuickElement("itunes:duration", "%02d:%02d:%02d" % (episode.hours, + episode.minutes, + episode.seconds)) + handler.addQuickElement("itunes:keywords", episode.keywords) + handler.addQuickElement("itunes:explicit", "no") + if episode.block: + handler.addQuickElement("itunes:block", "yes") + + def namespace_attributes(self): + return {"xmlns:itunes": "http://www.itunes.com/dtds/podcast-1.0.dtd"} + + +class AtomITunesFeedGenerator(ITunesElements, Atom1Feed): + def root_attributes(self): + atom_attrs = super(AtomITunesFeedGenerator, self).root_attributes() + atom_attrs.update(self.namespace_attributes()) + return atom_attrs + + +class RssITunesFeedGenerator(ITunesElements, Rss201rev2Feed): + def rss_attributes(self): + rss_attrs = super(RssITunesFeedGenerator, self).rss_attributes() + rss_attrs.update(self.namespace_attributes()) + return rss_attrs + + +class ShowFeed(Feed): + """ + A feed of podcasts for iTunes and other compatible podcatchers. + """ + def title(self, podcast): + return podcast.title + + def link(self, podcast): + return podcast.get_absolute_url() + + def categories(self, podcast): + return ("Music",) + + def feed_copyright(self, podcast): + return "{0} {1}".format(podcast.license, datetime.date.today().year) + + def ttl(self, podcast): + return podcast.ttl + + def items(self, podcast): + return podcast.episode_set.filter(status=1)[:300] + + def get_object(self, request, *args, **kwargs): + self.mime = [mc[0] for mc in MIME_CHOICES if mc[0] == kwargs["mime_type"]][0] + site = get_current_site(request) + self.podcast = get_object_or_404(Podcast, slug=kwargs["show_slug"]) + return self.podcast + + def item_title(self, episode): + return episode.title + + def item_description(self, episode): + "renders summary for atom" + return episode.description + + def item_link(self, episode): + return reverse("podcasting_episode_detail", + kwargs={"podcast_slug": self.podcast.slug, "slug": episode.slug}) + + # def item_author_link(self, episode): + # return "todo" #this one doesn't add anything in atom or rss + # + # def item_author_email(self, episode): + # return "todo" #this one doesn't add anything in atom or rss + + def item_pubdate(self, episode): + return episode.pub_date + + def item_categories(self, episode): + return self.categories(self.podcast) + + def item_enclosure_url(self, episode): + try: + e = episode.enclosure_set.get(mime=self.mime) + return e.url + except Enclosure.DoesNotExist: + pass + + def item_enclosure_length(self, episode): + try: + e = episode.enclosure_set.get(mime=self.mime) + return e.size + except Enclosure.DoesNotExist: + pass + + def item_enclosure_mime_type(self, episode): + try: + e = episode.enclosure_set.get(mime=self.mime) + return e.get_mime_display() + except Enclosure.DoesNotExist: + pass + + def item_keywords(self, episode): + return episode.keywords + + def feed_extra_kwargs(self, obj): + extra = {} + extra["podcast"] = self.podcast + return extra + + def item_extra_kwargs(self, item): + extra = {} + extra["podcast"] = self.podcast + extra["episode"] = item + return extra + + +class AtomShowFeed(ShowFeed): + feed_type = AtomITunesFeedGenerator + + def subtitle(self, show): + return show.subtitle + + def author_name(self, show): + return show.publisher + + def author_email(self, show): + return show.publisher_email + + def author_link(self, show): + return show.get_absolute_url() + + +class RssShowFeed(ShowFeed): + feed_type = RssITunesFeedGenerator + + def item_guid(self, episode): + "ITunesElements can't add isPermaLink attr unless None is returned here." + return None + + def description(self, show): + return show.description + + +class AtomRedirectView(RedirectView): + permanent = False + + def get_redirect_url(self, show_slug, mime_type): + return reverse( + "podcasts_show_feed_atom", + kwargs={"show_slug": show_slug, "mime_type": mime_type}) + + +class RssRedirectView(RedirectView): + permanent = False + + def get_redirect_url(self, show_slug, mime_type): + return reverse( + "podcasts_show_feed_rss", + kwargs={"show_slug": show_slug, "mime_type": mime_type}) diff --git a/app/podcasts/migrations/0001_initial.py b/app/podcasts/migrations/0001_initial.py new file mode 100644 index 0000000..644e7dc --- /dev/null +++ b/app/podcasts/migrations/0001_initial.py @@ -0,0 +1,60 @@ +# Generated by Django 4.1.3 on 2022-12-02 20:09 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('media', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Podcast', + fields=[ + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('title', models.CharField(max_length=255)), + ('subtitle', models.CharField(blank=True, max_length=255, null=True)), + ('slug', models.SlugField()), + ('publisher', models.CharField(max_length=255)), + ('publisher_email', models.CharField(max_length=255)), + ('description', models.TextField()), + ('keywords', models.CharField(blank=True, help_text='A comma-delimited list of words for searches, up to 12;', max_length=255)), + ('license', models.TextField(blank=True, null=True)), + ('featured_image', models.ForeignKey(blank=True, help_text='square JPEG (.jpg) or PNG (.png) image at a size of 1400x1400 pixels.', null=True, on_delete=django.db.models.deletion.CASCADE, to='media.luximage')), + ], + options={ + 'verbose_name': 'Podcast', + 'verbose_name_plural': 'Podcasts', + 'ordering': ('title', 'slug'), + }, + ), + migrations.CreateModel( + name='Episode', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('title', models.CharField(max_length=255)), + ('subtitle', models.CharField(blank=True, max_length=255, null=True)), + ('date_created', models.DateTimeField(auto_now_add=True)), + ('date_updated', models.DateTimeField(auto_now=True)), + ('pub_date', models.DateTimeField(blank=True, null=True)), + ('enable_comments', models.BooleanField(default=True)), + ('slug', models.SlugField()), + ('status', models.IntegerField(choices=[(0, 'Draft'), (1, 'Published')], default=0)), + ('description', models.TextField()), + ('keywords', models.CharField(blank=True, help_text='A comma-delimited list of words for searches, up to 12;', max_length=255)), + ('featured_image', models.ForeignKey(blank=True, help_text='square JPEG (.jpg) or PNG (.png) image at a size of 1400x1400 pixels.', null=True, on_delete=django.db.models.deletion.CASCADE, to='media.luximage')), + ('podcast', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='podcasts.podcast')), + ], + options={ + 'verbose_name': 'Episode', + 'verbose_name_plural': 'Episodes', + 'ordering': ('-pub_date', 'slug'), + }, + ), + ] diff --git a/app/podcasts/migrations/__init__.py b/app/podcasts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/app/podcasts/migrations/__init__.py diff --git a/app/podcasts/models.py b/app/podcasts/models.py new file mode 100644 index 0000000..b574986 --- /dev/null +++ b/app/podcasts/models.py @@ -0,0 +1,132 @@ +import os +import uuid +from django.db import models +from django.urls import reverse +from django.template.defaultfilters import slugify +from django.conf import settings + +# optional external dependencies +try: + from licenses.models import License +except: + License = None + +from taggit.managers import TaggableManager +from mutagen.mp3 import MP3 + +from media.models import LuxAudio, LuxImage + +def get_show_upload_folder(instance, pathname): + "A standardized pathname for uploaded files and images." + root, ext = os.path.splitext(pathname) + return "{0}/podcasts/{1}/{2}{3}".format( + settings.PODCASTING_IMG_PATH, instance.slug, slugify(root), ext + ) + + +def get_episode_upload_folder(instance, pathname): + "A standardized pathname for uploaded files and images." + root, ext = os.path.splitext(pathname) + if instance.shows.count() == 1: + return "{0}/podcasts/{1}/episodes/{2}{3}".format( + settings.PODCASTING_IMG_PATH, instance.shows.all()[0].slug, slugify(root), ext + ) + else: + return "{0}/podcasts/episodes/{1}/{2}{3}".format( + settings.PODCASTING_IMG_PATH, instance.slug, slugify(root), ext + ) + +MIME_CHOICES = ( + ("mp3", "audio/mpeg"), + ("mp4", "audio/mp4"), + ("ogg", "audio/ogg"), +) + + +#audio = MP3("example.mp3") +#print(audio.info.length) + +class Podcast(models.Model): + uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + title = models.CharField(max_length=255) + subtitle = models.CharField(max_length=255, blank=True, null=True) + slug = models.SlugField() + publisher = models.CharField(max_length=255) + publisher_email = models.CharField(max_length=255) + description = models.TextField() + keywords = models.CharField(max_length=255, blank=True, help_text="A comma-delimited list of words for searches, up to 12;") + license = models.TextField(blank=True, null=True) + featured_image = models.ForeignKey(LuxImage, on_delete=models.CASCADE, null=True, blank=True, help_text=("square JPEG (.jpg) or PNG (.png) image at a size of 1400x1400 pixels.")) + + + class Meta: + verbose_name = "Podcast" + verbose_name_plural = "Podcasts" + ordering = ("title", "slug") + + def __str__(self): + return self.title + + def get_absolute_url(self): + return reverse("podcasts:list", kwargs={"slug": self.slug}) + + def ttl(self): + return "1440" + + +class Episode(models.Model): + """ + An individual podcast episode and it's unique attributes. + """ + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + title = models.CharField(max_length=255) + subtitle = models.CharField(max_length=255, blank=True, null=True) + date_created = models.DateTimeField(auto_now_add=True, editable=False) + date_updated = models.DateTimeField(auto_now=True, editable=False) + pub_date = models.DateTimeField(null=True, blank=True) + podcast = models.ForeignKey(Podcast, on_delete=models.PROTECT) + enable_comments = models.BooleanField(default=True) + slug = models.SlugField() + PUB_STATUS = ( + (0, 'Draft'), + (1, 'Published'), + ) + status = models.IntegerField(choices=PUB_STATUS, default=0) + description = models.TextField() + featured_image = models.ForeignKey(LuxImage, on_delete=models.CASCADE, null=True, blank=True, help_text=("square JPEG (.jpg) or PNG (.png) image at a size of 1400x1400 pixels.")) + # iTunes specific fields + keywords = models.CharField(max_length=255, blank=True, help_text="A comma-delimited list of words for searches, up to 12;") + + class Meta: + verbose_name = "Episode" + verbose_name_plural = "Episodes" + ordering = ("-pub_date", "slug") + + def __str__(self): + return self.title + + def get_absolute_url(self): + return reverse("podcasting_episode_detail", kwargs={"show_slug": self.shows.all()[0].slug, "slug": self.slug}) + + def get_next(self): + next = self.__class__.objects.filter(published__gt=self.published) + try: + return next[0] + except IndexError: + return False + + def get_prev(self): + prev = self.__class__.objects.filter(published__lt=self.published).order_by("-published") + try: + return prev[0] + except IndexError: + return False + + def get_explicit_display(self): + return "no" + + def seconds_total(self): + try: + return self.minutes * 60 + self.seconds + except: + return 0 diff --git a/app/podcasts/templates/podcasts/detail.html b/app/podcasts/templates/podcasts/detail.html new file mode 100644 index 0000000..361d822 --- /dev/null +++ b/app/podcasts/templates/podcasts/detail.html @@ -0,0 +1,108 @@ +{% extends 'base.html' %} +{% load typogrify_tags %} +{% load comments %} +{% block pagetitle %}{{object.title|striptags}} - by Scott Gilbertson{% endblock %} +{% block metadescription %}{% autoescape on %}{{object.meta_description|striptags|safe}}{% endautoescape %}{% endblock %} +{%block extrahead%} + <link rel="stylesheet" href="/media/src/solarized.css" type="text/css" media="screen"/> +{%endblock%} + +{% block bodyid %}class="detail single"{% endblock %} +{% block breadcrumbs %}{% include "lib/breadcrumbs.html" with breadcrumbs=breadcrumbs %}{% endblock %} +{% block primary %}<main role="main"> + <article class="h-entry hentry entry-content content" itemscope itemType="http://schema.org/BlogPosting"> + <header id="header" class="post-header"> + <h1 class="p-name post-title" itemprop="headline">{{object.title|smartypants|safe}}</h1> + <h2 class="post-subtitle">{% if object.subtitle %}{{object.subtitle|smartypants|safe}}{%else%}{{object.meta_description|safe|smartypants|widont}}{%endif%}</h2> + <div class="post-dateline"> + {% if object.originally_published_by %}<h4 class="post-source">Originally Published By: <a href="{{object.originally_published_by_url}}" title="View {{object.title}} on {{object.originally_published_by}}">{{object.originally_published_by}}</a></h4>{%endif%} + <time class="dt-published published dt-updated post-date" datetime="{{object.pub_date|date:'c'}}" itemprop="datePublished">{{object.pub_date|date:"F"}} <span>{{object.pub_date|date:"j, Y"}}</span></time> + <span class="hide" itemprop="author" itemscope itemtype="http://schema.org/Person">by <a class="p-author h-card" href="/about"><span itemprop="name">Scott Gilbertson</span></a></span> + </div> + </header> + + + <div id="article" class="e-content entry-content post-article post-body-{% with object.template_name as t %}{%if t == 0 or t == 2 %}single{%endif%}{%if t == 1 or t == 3 %}double{%endif%}{%endwith%}" itemprop="articleBody"> + {{object.body_html|safe|smartypants|widont}} + </div> + </article> + <div class="entry-footer"> + <aside class="narrow donate"> + <h3>Support</h3> + <p>Want to help support Lulu and Birdie? You can <a href="/bookshop/">buy the book</a>, or you can donate a few dollars.</p> + <div class="donate-btn"> + <form action="https://www.paypal.com/cgi-bin/webscr" method="post" target="_top"> + <input type="hidden" name="cmd" value="_s-xclick"> + <input type="hidden" name="hosted_button_id" value="HYJFZQSBGJ8QQ"> + <input type="submit" name="submit" title="Donate to luxagraf via PayPal"> + </form> + </div> + <div class="donate-btn"> + <a class="liberapay-btn" href="https://liberapay.com/luxagraf/donate"><span>Donate</span></a> + </div> + </aside> + {% comment %}<aside class="narrow join"> + <h3>Subscribe</h3> + <p>You're reading <code>/src/</code>, a collection of infrequent postings about open source software, linux tools, and other nerdry. If you'd like to join us, drop your email in the form below: </p> + <iframe target='_parent' style="border:none !important; background:white; width:100% !important;" title="embedded form for subscribing the the src newsletter" src="{% url 'lttr:subscribe' slug='range' %}"></iframe> + <p>Unsubscribing is easy. It's <a href="/src/building-your-own-mailing-list-software">self-hosted</a> and <a href="/privacy" title="My privacy policy">respects your privacy</a>. If you don't want an email, there's also <a href="/src/feed.xml">an RSS feed</a>, and it's all archived <a href="/src/">here</a>.</p> + </aside>{% endcomment%} + </div> + {% with object.get_next_published as next %} + {% with object.get_previous_published as prev %} + <nav class="page-navigation"> + <div>{% if prev%} + <span class="label">Previous:</span> + <a href="{{ prev.get_absolute_url }}" rel="prev" title=" {{prev.title}}">{{prev.title|safe}}</a> + </div>{%endif%}{% if next%} + <div> + <span class="label">Next:</span> + <a href="{{ next.get_absolute_url }}" rel="next" title=" {{next.title}}">{{next.title|safe}}</a> + </div>{%endif%} + </nav>{%endwith%}{%endwith%} + {% if object.related.all %}<div class="article-afterward related"> + <div class="related-bottom"> + <h6 class="hedtinycaps">You might also enjoy</h6> + <div class="archive-grid-quad">{% for object in related %} + <div class="archive-grid-card archive-grid-card-simple"> + <a href="{{object.get_absolute_url}}" title="{{object.title}}"> + <div class="card-image-tiny"> + {% if object.featured_image %} + {% include "lib/img_archive.html" with image=object.featured_image nolightbox=True %} + {%endif%} + </div> + <h4 class="p-name card-hed" itemprop="headline">{% if object.title %}{{object.title|safe|smartypants|widont}}{% else %}{{object.common_name}}{%endif%}</h4> + <p class="p-author author hide" itemprop="author"><span class="byline-author" itemscope itemtype="http://schema.org/Person"><span itemprop="name">Scott Gilbertson</span></span></p> + <span class="card-smcaps"> + {% if object.location %}<span class="p-location h-adr adr card-location" itemprop="contentLocation" itemscope itemtype="http://schema.org/Place"> + {% if object.location.country_name == "United States" %}{{object.location.state_name}}{%else%}{{object.location.country_name}}{%endif%} + </span>{%endif%} + {% if object.location and object.model_name.model != 'page' %}–{%endif%} + {% if object.model_name.model != 'page' %}<time class="dt-published published dt-updated" datetime="{{object.pub_date|date:'c'}}"><span>{{object.pub_date|date:" Y"}}</span></time>{%endif%} + </span> + </a> + </div> + {% endfor %}</div> + </div> + </div>{%endif%} + </main> + {% if object.enable_comments %} +{% get_comment_count for object as comment_count %} +{%if comment_count > 0 %} +<div class="comment-wrapper"> +<p class="comments-header">{{comment_count}} Comment{{ comment_count|pluralize }}</p> +{% render_comment_list for object %} +{%endif%} +<div class="comment-form-wrapper {%if comment_count > 0%}comment-form-border{%endif%}"> +{% render_comment_form for object %} +</div> +{% else %} +<p class="comments--header" style="text-align: center">Sorry, comments have been disabled for this post.</p> +{%endif%} +</div> +</main> +{% endblock %} +{% block js %} +<script src="/media/js/leaflet-master/leaflet-mod.js"></script> +<script src="/media/js/detail.min.js"></script> +{%endblock%} diff --git a/app/podcasts/templates/podcasts/list.html b/app/podcasts/templates/podcasts/list.html new file mode 100644 index 0000000..89b7ea8 --- /dev/null +++ b/app/podcasts/templates/podcasts/list.html @@ -0,0 +1,31 @@ +{% extends 'base.html' %} +{% load typogrify_tags %} +{% load pagination_tags %} +{% load comments %} + +{% block pagetitle %}The Lulu and Birdie Podcast{% endblock %} +{% block metadescription %}The Adventures of Lulu, Birdie, and Henry in podcast form - by Scott Gilbertson.{% endblock %} +{% block breadcrumbs %}{% include "lib/breadcrumbs.html" with breadcrumbs=breadcrumbs %}{% endblock %} +{% block primary %}<main role="main" class="archive-wrapper"> + <div class="archive-intro"> + <h1 class="archive-hed">{{podcast.title}}</h1> + {% if object.subtitle %}<h2 class="list-subhed">{{podcast.subtitle}}</h2>{% endif %} + </div> + + <h1 class="archive-sans">Episodes</h1>{% autopaginate object_list 24 %} + <ul class="archive-list">{% for object in object_list %} + <li class="h-entry hentry archive-list-card archive-list-card-sm" itemscope itemType="http://schema.org/Article"> + <span class="date dt-published card-smcaps">{{object.pub_date|date:"F Y"}}</span> + <a href="{{object.get_absolute_url}}"> + <h2 class="card-hed">{{object.title|safe|smartypants|widont}}</h2> + <p class="p-summary card-lede">{% if object.subtitle %}{{object.subtitle}}{%else%}{{object.meta_description|safe|smartypants|widont}}{%endif%}</p> + </a> + </li> + {%endfor%}</ul> + <a href="{% url 'podcasts_show_feed_atom' podcast.slug 'mp3' %}" >MP3</a> + <a href="{% url 'podcasts_show_feed_atom' podcast.slug 'mp4' %}" >MP4</a> + <a href="{% url 'podcasts_show_feed_rss' podcast.slug 'mp3' %}" >OGG</a> + + + </main> +{%endblock%} diff --git a/app/podcasts/urls.py b/app/podcasts/urls.py new file mode 100644 index 0000000..622b203 --- /dev/null +++ b/app/podcasts/urls.py @@ -0,0 +1,19 @@ +from django.urls import path, re_path + +from . import views + +app_name = "podcasts" + +urlpatterns = [ + re_path( + r'<str:slug>/<int:page>', + views.PodcastListView.as_view(), + name="list" + ), + path( + r'<str:slug>/', + views.PodcastListView.as_view(), + {'page':1}, + name="list" + ), +] diff --git a/app/podcasts/urls_feeds.py b/app/podcasts/urls_feeds.py new file mode 100644 index 0000000..07e02ae --- /dev/null +++ b/app/podcasts/urls_feeds.py @@ -0,0 +1,17 @@ +from django.urls import include, re_path + +from .feeds import RssShowFeed, AtomShowFeed, AtomRedirectView, RssRedirectView +from .models import MIME_CHOICES + + +MIMES = "|".join([enclosure[0] for enclosure in MIME_CHOICES]) + + +urlpatterns = [ + # Episode list feed by podcast (RSS 2.0 and iTunes) + re_path(r"^(?P<show_slug>[-\w]+)/(?P<mime_type>{mimes})/rss/$".format(mimes=MIMES), + RssShowFeed(), name="podcasts_show_feed_rss"), + # Episode list feed by show (Atom) + re_path(r"^(?P<show_slug>[-\w]+)/(?P<mime_type>{mimes})/atom/$".format(mimes=MIMES), + AtomShowFeed(), name="podcasts_show_feed_atom"), +] diff --git a/app/podcasts/views.py b/app/podcasts/views.py new file mode 100644 index 0000000..a8edbfa --- /dev/null +++ b/app/podcasts/views.py @@ -0,0 +1,29 @@ +from django.views.generic import ListView +from django.views.generic.detail import DetailView +from django.views.generic.dates import DateDetailView +from django.urls import reverse +from django.contrib.syndication.views import Feed +from django.apps import apps +from django.shortcuts import get_object_or_404 +from django.conf import settings +from django.db.models import Q + +from utils.views import PaginatedListView + +from .models import Episode, Podcast + + +class PodcastListView(PaginatedListView): + """ + Return a list of Episodes in reverse chronological order + """ + model = Podcast + template_name = "podcasts/list.html" + queryset = Episode.objects.filter(podcast=1).filter(status__exact=1).order_by('-pub_date') + + def get_context_data(self, **kwargs): + context = super(PodcastListView, self).get_context_data(**kwargs) + context['breadcrumbs'] = ['podcast',] + context['podcast'] = Podcast.objects.get(title="The Lulu & Birdie Podcast") + return context + |