feat: add error message page; improve admin ui; add missing test files
This commit is contained in:
parent
a0468899b0
commit
876e4cdfa7
|
@ -3,7 +3,7 @@ from django.urls import include, path, reverse
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
|
|
||||||
from .models import Comment, Diffusion, Program
|
from . import models
|
||||||
from .views.admin import StatisticsView
|
from .views.admin import StatisticsView
|
||||||
|
|
||||||
__all__ = ["AdminSite"]
|
__all__ = ["AdminSite"]
|
||||||
|
@ -26,17 +26,20 @@ class AdminSite(admin.AdminSite):
|
||||||
context.update(
|
context.update(
|
||||||
{
|
{
|
||||||
# all programs
|
# all programs
|
||||||
"programs": Program.objects.active()
|
"programs": models.Program.objects.active()
|
||||||
.values("pk", "title")
|
.values("pk", "title")
|
||||||
.order_by("title"),
|
.order_by("title"),
|
||||||
# today's diffusions
|
# today's diffusions
|
||||||
"diffusions": Diffusion.objects.date()
|
"diffusions": models.Diffusion.objects.date()
|
||||||
.order_by("start")
|
.order_by("start")
|
||||||
.select_related("episode"),
|
.select_related("episode"),
|
||||||
# TODO: only for dashboard
|
# TODO: only for dashboard
|
||||||
# last comments
|
# last comments
|
||||||
"comments": Comment.objects.order_by("-date").select_related(
|
"comments": models.Comment.objects.order_by(
|
||||||
"page"
|
"-date"
|
||||||
|
).select_related("page")[0:10],
|
||||||
|
"latests": models.Page.objects.select_subclasses().order_by(
|
||||||
|
"-pub_date"
|
||||||
)[0:10],
|
)[0:10],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
60
aircox/controllers/diffusion_monitor.py
Normal file
60
aircox/controllers/diffusion_monitor.py
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
from datetime import datetime, time
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
|
from django.utils import timezone as tz
|
||||||
|
|
||||||
|
from aircox.models import Diffusion, Schedule
|
||||||
|
|
||||||
|
logger = logging.getLogger("aircox.commands")
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ("DiffusionMonitor",)
|
||||||
|
|
||||||
|
|
||||||
|
class DiffusionMonitor:
|
||||||
|
"""Handle generation and update of Diffusion instances."""
|
||||||
|
|
||||||
|
date = None
|
||||||
|
|
||||||
|
def __init__(self, date):
|
||||||
|
self.date = date or date.today()
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
episodes, diffusions = [], []
|
||||||
|
for schedule in Schedule.objects.filter(
|
||||||
|
program__active=True, initial__isnull=True
|
||||||
|
):
|
||||||
|
eps, diffs = schedule.diffusions_of_month(self.date)
|
||||||
|
if eps:
|
||||||
|
episodes += eps
|
||||||
|
if diffs:
|
||||||
|
diffusions += diffs
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"[update] %s: %d episodes, %d diffusions and reruns",
|
||||||
|
str(schedule),
|
||||||
|
len(eps),
|
||||||
|
len(diffs),
|
||||||
|
)
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
logger.info(
|
||||||
|
"[update] save %d episodes and %d diffusions",
|
||||||
|
len(episodes),
|
||||||
|
len(diffusions),
|
||||||
|
)
|
||||||
|
for episode in episodes:
|
||||||
|
episode.save()
|
||||||
|
for diffusion in diffusions:
|
||||||
|
# force episode id's update
|
||||||
|
diffusion.episode = diffusion.episode
|
||||||
|
diffusion.save()
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
qs = Diffusion.objects.filter(
|
||||||
|
type=Diffusion.TYPE_UNCONFIRMED,
|
||||||
|
start__lt=tz.make_aware(datetime.combine(self.date, time.min)),
|
||||||
|
)
|
||||||
|
logger.info("[clean] %d diffusions will be removed", qs.count())
|
||||||
|
qs.delete()
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
@ -11,7 +11,7 @@ from argparse import RawTextHelpFormatter
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.utils import timezone as tz
|
from django.utils import timezone as tz
|
||||||
|
|
||||||
from aircox.controllers.diffusions import Diffusions
|
from aircox.controllers.diffusion_monitor import DiffusionMonitor
|
||||||
|
|
||||||
logger = logging.getLogger("aircox.commands")
|
logger = logging.getLogger("aircox.commands")
|
||||||
|
|
||||||
|
@ -70,7 +70,7 @@ class Command(BaseCommand):
|
||||||
date += tz.timedelta(days=28)
|
date += tz.timedelta(days=28)
|
||||||
date = date.replace(day=1)
|
date = date.replace(day=1)
|
||||||
|
|
||||||
actions = Diffusions(date)
|
actions = DiffusionMonitor(date)
|
||||||
if options.get("update"):
|
if options.get("update"):
|
||||||
actions.update()
|
actions.update()
|
||||||
if options.get("clean"):
|
if options.get("clean"):
|
||||||
|
|
|
@ -278,6 +278,16 @@ class Comment(models.Model):
|
||||||
date = models.DateTimeField(auto_now_add=True)
|
date = models.DateTimeField(auto_now_add=True)
|
||||||
content = models.TextField(_("content"), max_length=1024)
|
content = models.TextField(_("content"), max_length=1024)
|
||||||
|
|
||||||
|
item_template_name = "aircox/widgets/comment_item.html"
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def parent(self):
|
||||||
|
"""Return Page as its subclass."""
|
||||||
|
return Page.objects.select_subclasses().filter(id=self.page_id).first()
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return self.parent.get_absolute_url()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Comment")
|
verbose_name = _("Comment")
|
||||||
verbose_name_plural = _("Comments")
|
verbose_name_plural = _("Comments")
|
||||||
|
|
|
@ -65,7 +65,10 @@
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
{# Today's diffusions #}
|
{# Today's diffusions #}
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
<span class="navbar-link">{% translate "Today" %}</span>
|
<span class="icon-text navbar-link">
|
||||||
|
<span class="icon"><i class="fa-regular fa-calendar-days"></i></span>
|
||||||
|
<span>{% translate "Today" %}</span>
|
||||||
|
</span>
|
||||||
<div class="navbar-dropdown is-boxed">
|
<div class="navbar-dropdown is-boxed">
|
||||||
{% for diffusion in diffusions %}
|
{% for diffusion in diffusions %}
|
||||||
<a class="navbar-item {% if diffusion.is_now %}has-background-primary{% endif %}" href="{% url "admin:aircox_episode_change" diffusion.episode.pk %}">
|
<a class="navbar-item {% if diffusion.is_now %}has-background-primary{% endif %}" href="{% url "admin:aircox_episode_change" diffusion.episode.pk %}">
|
||||||
|
@ -76,23 +79,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Programs #}
|
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
|
||||||
<a class="navbar-link" href="{% url "admin:aircox_program_changelist" %}">{% translate "Programs" %}</a>
|
|
||||||
<div class="navbar-dropdown is-boxed">
|
|
||||||
<input type="text" onkeyup="aircox.filter_menu(event)"
|
|
||||||
placeholder="{% translate "Search" %}" class="navbar-item input" />
|
|
||||||
<hr class="navbar-divider"/>
|
|
||||||
{% for program in programs %}
|
|
||||||
<a class="navbar-item" href="{% url "admin:aircox_program_change" program.pk %}">
|
|
||||||
{{ program.title }}</a>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{# Articles #}
|
{# Articles #}
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
<a class="navbar-link" href="{% url "admin:aircox_article_changelist" %}">{% translate "Articles" %}</a>
|
<a class="icon-text navbar-link" href="{% url "admin:aircox_article_changelist" %}">
|
||||||
|
<span class="icon"><i class="fa fa-newspaper"></i></span>
|
||||||
|
<span>{% translate "Articles" %}</span>
|
||||||
|
</a>
|
||||||
<div class="navbar-dropdown is-boxed">
|
<div class="navbar-dropdown is-boxed">
|
||||||
<input type="text" onkeyup="aircox.filter_menu(event)"
|
<input type="text" onkeyup="aircox.filter_menu(event)"
|
||||||
placeholder="{% translate "Search" %}" class="navbar-item input" />
|
placeholder="{% translate "Search" %}" class="navbar-item input" />
|
||||||
|
@ -104,9 +96,29 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{# Programs #}
|
||||||
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
|
<a class="icon-text navbar-link" href="{% url "admin:aircox_program_changelist" %}">
|
||||||
|
<span class="icon"><i class="fa fa-folder"></i></span>
|
||||||
|
<span>{% translate "Programs" %}</span>
|
||||||
|
</a>
|
||||||
|
<div class="navbar-dropdown is-boxed">
|
||||||
|
<input type="text" onkeyup="aircox.filter_menu(event)"
|
||||||
|
placeholder="{% translate "Search" %}" class="navbar-item input" />
|
||||||
|
<hr class="navbar-divider"/>
|
||||||
|
{% for program in programs %}
|
||||||
|
<a class="navbar-item" href="{% url "admin:aircox_program_change" program.pk %}">
|
||||||
|
{{ program.title }}</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{# Episodes #}
|
{# Episodes #}
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
<a class="navbar-link" href="{% url "admin:aircox_episode_changelist" %}">{% translate "Episodes" %}</a>
|
<a class="icon-text navbar-link" href="{% url "admin:aircox_episode_changelist" %}">
|
||||||
|
<span class="icon"><i class="fa fa-calendar-check"></i></span>
|
||||||
|
<span>{% translate "Episodes" %}</span>
|
||||||
|
</a>
|
||||||
<div class="navbar-dropdown is-boxed">
|
<div class="navbar-dropdown is-boxed">
|
||||||
<input type="text" onkeyup="aircox.filter_menu(event)"
|
<input type="text" onkeyup="aircox.filter_menu(event)"
|
||||||
placeholder="{% translate "Search" %}" class="navbar-item input" />
|
placeholder="{% translate "Search" %}" class="navbar-item input" />
|
||||||
|
@ -121,7 +133,10 @@
|
||||||
|
|
||||||
<div class="navbar-end">
|
<div class="navbar-end">
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
<a href="#" class="navbar-link">{% translate "Tools" %}</a>
|
<a href="#" class="icon-text navbar-link">
|
||||||
|
<span class="icon"><i class="fa-solid fa-screwdriver-wrench"></i></span>
|
||||||
|
<span>{% translate "Tools" %}</span>
|
||||||
|
</a>
|
||||||
<div class="navbar-dropdown is-boxed is-right">
|
<div class="navbar-dropdown is-boxed is-right">
|
||||||
{% get_admin_tools as admin_tools %}
|
{% get_admin_tools as admin_tools %}
|
||||||
{% for label, url in admin_tools %}
|
{% for label, url in admin_tools %}
|
||||||
|
@ -131,8 +146,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
<a href="{% url "admin:auth_user_change" user.pk %}" class="navbar-link">
|
<a href="{% url "admin:auth_user_change" user.pk %}" class="icon-text navbar-link">
|
||||||
{% firstof user.get_short_name user.get_username %}
|
<span class="icon"><i class="fa fa-user"></i></span>
|
||||||
|
<span>{% firstof user.get_short_name user.get_username %}</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="navbar-dropdown is-boxed is-right">
|
<div class="navbar-dropdown is-boxed is-right">
|
||||||
{% block userlinks %}
|
{% block userlinks %}
|
||||||
|
@ -179,16 +195,18 @@
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<div id="content" class="{% block coltype %}colM{% endblock %}">
|
{% block app %}
|
||||||
{% block pretitle %}{% endblock %}
|
<div id="content" class="{% block coltype %}colM{% endblock %}">
|
||||||
{% block content_title %}{% if title %}<h1 class="title is-3">{{ title }}</h1>{% endif %}{% endblock %}
|
{% block pretitle %}{% endblock %}
|
||||||
{% block content %}
|
{% block content_title %}{% if title %}<h1 class="title is-3">{{ title }}</h1>{% endif %}{% endblock %}
|
||||||
{% block object-tools %}{% endblock %}
|
{% block content %}
|
||||||
{{ content }}
|
{% block object-tools %}{% endblock %}
|
||||||
|
{{ content }}
|
||||||
|
{% endblock %}
|
||||||
|
{% block sidebar %}{% endblock %}
|
||||||
|
<br class="clear">
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block sidebar %}{% endblock %}
|
|
||||||
<br class="clear">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- END Content -->
|
<!-- END Content -->
|
||||||
|
|
||||||
|
@ -197,7 +215,9 @@
|
||||||
<!-- END Container -->
|
<!-- END Container -->
|
||||||
|
|
||||||
{% block player %}
|
{% block player %}
|
||||||
|
{% if request.station %}
|
||||||
<div id="player">{% include "aircox/widgets/player.html" %}</div>
|
<div id="player">{% include "aircox/widgets/player.html" %}</div>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -2,92 +2,92 @@
|
||||||
{% load i18n thumbnail %}
|
{% load i18n thumbnail %}
|
||||||
|
|
||||||
|
|
||||||
{% block messages %}
|
{% block app %}
|
||||||
{{ block.super }}
|
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<h1 class="title is-4">{% translate "Today" %}</h1>
|
<div class="box">
|
||||||
<table class="table is-fullwidth is-striped">
|
<h1 class="title icon-text is-4">
|
||||||
<tbody>
|
<span class="icon"><i class="fa-regular fa-calendar-days"></i></span>
|
||||||
{% for diffusion in diffusions %}
|
<span>{% translate "Today" %}</span>
|
||||||
{% with episode=diffusion.episode %}
|
</h1>
|
||||||
<tr {% if diffusion.is_now %}class="is-selected"{% endif %}>
|
{% if diffusions %}
|
||||||
<td>{{ diffusion.start|time }} - {{ diffusion.end|time }}</td>
|
<table class="table is-fullwidth is-striped">
|
||||||
<td><img src="{% thumbnail episode.cover 64x64 crop %}"/></td>
|
<tbody>
|
||||||
<td>
|
{% for diffusion in diffusions %}
|
||||||
<a href="{% url "admin:aircox_episode_change" episode.pk %}">{{ episode.title }}</a>
|
{% with episode=diffusion.episode %}
|
||||||
|
<tr {% if diffusion.is_now %}class="is-selected"{% endif %}>
|
||||||
{% if diffusion.type == diffusion.TYPE_ON_AIR %}
|
<td>{{ diffusion.start|time }} - {{ diffusion.end|time }}</td>
|
||||||
<span class="tag is-info">
|
<td><img src="{% thumbnail episode.cover 64x64 crop %}"/></td>
|
||||||
<span class="icon is-small">
|
<td>
|
||||||
{% if diffusion.is_live %}
|
<a href="{% url "admin:aircox_episode_change" episode.pk %}">{{ episode.title }}</a>
|
||||||
<i class="fa fa-microphone"
|
|
||||||
title="{% translate "Live diffusion" %}"></i>
|
|
||||||
{% else %}
|
|
||||||
<i class="fa fa-music"
|
|
||||||
title="{% translate "Differed diffusion" %}"></i>
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{{ diffusion.get_type_display }}
|
{% if diffusion.type == diffusion.TYPE_ON_AIR %}
|
||||||
</span>
|
<span class="tag is-info">
|
||||||
{% elif diffusion.type == diffusion.TYPE_CANCEL %}
|
<span class="icon is-small">
|
||||||
<span class="tag is-danger">
|
{% if diffusion.is_live %}
|
||||||
{{ diffusion.get_type_display }}</span>
|
<i class="fa fa-microphone"
|
||||||
{% elif diffusion.type == diffusion.TYPE_UNCONFIRMED %}
|
title="{% translate "Live diffusion" %}"></i>
|
||||||
<span class="tag is-warning">
|
{% else %}
|
||||||
{{ diffusion.get_type_display }}</span>
|
<i class="fa fa-music"
|
||||||
{% endif %}
|
title="{% translate "Differed diffusion" %}"></i>
|
||||||
</td>
|
{% endif %}
|
||||||
</tr>
|
</span>
|
||||||
{% endwith %}
|
|
||||||
{% endfor %}
|
{{ diffusion.get_type_display }}
|
||||||
</tbody>
|
</span>
|
||||||
</table>
|
{% elif diffusion.type == diffusion.TYPE_CANCEL %}
|
||||||
</div>
|
<span class="tag is-danger">
|
||||||
<div class="column">
|
{{ diffusion.get_type_display }}</span>
|
||||||
<h1 class="title is-4">{% translate "Latest comments" %}</h1>
|
{% elif diffusion.type == diffusion.TYPE_UNCONFIRMED %}
|
||||||
<table class="table is-fullwidth is-striped">
|
<span class="tag is-warning">
|
||||||
{% for comment in comments %}
|
{{ diffusion.get_type_display }}</span>
|
||||||
{% with page=comment.page %}
|
{% endif %}
|
||||||
<tr>
|
</td>
|
||||||
<th>
|
</tr>
|
||||||
{{ page.title }}
|
{% endwith %}
|
||||||
</a>
|
{% endfor %}
|
||||||
|
|
</tbody>
|
||||||
<span title="{{ comment.email }}">{{ comment.nickname }}</span>
|
</table>
|
||||||
—
|
{% else %}
|
||||||
<span>{{ comment.date }}</span>
|
<div class="block has-text-centered">
|
||||||
<span class="float-right">
|
{% trans "No diffusion is scheduled for today." %}
|
||||||
<a href="{% url "admin:aircox_comment_change" comment.pk %}"
|
</div>
|
||||||
title="{% translate "Edit comment" %}"
|
{% endif %}
|
||||||
aria-label="{% translate "Edit comment" %}">
|
</div>
|
||||||
<span class="fa fa-edit"></span>
|
|
||||||
</a>
|
<div class="box">
|
||||||
<a class="has-text-danger"
|
<h1 class="title is-4 icon-text">
|
||||||
title="{% translate "Delete comment" %}"
|
<span class="icon"><i class="fa-regular fa-comments"></i></span>
|
||||||
aria-label="{% translate "Delete comment" %}"
|
<span>{% translate "Latest comments" %}</span>
|
||||||
href="{% url "admin:aircox_comment_delete" comment.pk %}">
|
</h1>
|
||||||
<span class="fa fa-trash-alt"></span>
|
{% if comments %}
|
||||||
</a>
|
{% include "aircox/widgets/page_list.html" with object_list=comments with_title=True %}
|
||||||
</th>
|
<div class="has-text-centered">
|
||||||
</tr>
|
<a href="{% url "admin:aircox_comment_changelist" %}" class="float-center">{% translate "All comments" %}</a>
|
||||||
<tr>
|
</div>
|
||||||
<td colspan="2">
|
{% else %}
|
||||||
{{ comment.content|slice:"0:128" }}
|
<p class="block has-text-centered">{% trans "No comment posted yet" %}</p>
|
||||||
</td>
|
{% endif %}
|
||||||
</tr>
|
|
||||||
{% endwith %}
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div class="has-text-centered">
|
|
||||||
<a href="{% url "admin:aircox_comment_changelist" %}" class="float-center">{% translate "All comments" %}</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
|
<div class="box">
|
||||||
|
<h1 class="title is-4 icon-text">
|
||||||
|
<span class="icon"><i class="fa-regular fa-newspaper"></i></span>
|
||||||
|
<span>{% translate "Latest publications" %}</span>
|
||||||
|
</h1>
|
||||||
|
{% if latests %}
|
||||||
|
{% include "aircox/widgets/page_list.html" with object_list=latests no_actions=True %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="box">
|
||||||
|
<h1 class="title is-4 icon-text">
|
||||||
|
<span class="icon"><i class="fa fa-screwdriver-wrench"></i></span>
|
||||||
|
<span>{% translate "Administration" %}</span>
|
||||||
|
</h1>
|
||||||
|
{% include "admin/app_list.html" with app_list=app_list show_changelinks=True %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -48,6 +48,7 @@ Usefull context:
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
|
{% block top-nav-container %}
|
||||||
<nav class="navbar has-shadow" role="navigation" aria-label="main navigation">
|
<nav class="navbar has-shadow" role="navigation" aria-label="main navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand">
|
<div class="navbar-brand">
|
||||||
|
@ -84,6 +85,7 @@ Usefull context:
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="columns is-desktop">
|
<div class="columns is-desktop">
|
||||||
|
@ -161,6 +163,8 @@ Usefull context:
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
</div>
|
</div>
|
||||||
|
{% block player-container %}
|
||||||
<div id="player">{% include "aircox/widgets/player.html" %}</div>
|
<div id="player">{% include "aircox/widgets/player.html" %}</div>
|
||||||
|
{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
30
aircox/templates/aircox/errors/base.html
Normal file
30
aircox/templates/aircox/errors/base.html
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{% extends "aircox/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block top-nav-container %}
|
||||||
|
{% if request.station %}{{ block.super }}{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block player-container %}
|
||||||
|
{% if request.station %}{{ block.super }}{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block head_title %}
|
||||||
|
{% block title %}{% trans "An error occurred..." %}{% endblock %}
|
||||||
|
{% if request.station %}
|
||||||
|
—
|
||||||
|
{{ station.name }}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="message is-danger">
|
||||||
|
<div class="message-header">
|
||||||
|
<p>{% block error_title %}{% trans "An error occurred" %}{% endblock %}</p>
|
||||||
|
</div>
|
||||||
|
<div class="message-body">
|
||||||
|
{% block error_content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
22
aircox/templates/aircox/errors/no_station.html
Normal file
22
aircox/templates/aircox/errors/no_station.html
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{% extends "aircox/errors/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block error_title %}{% trans "No station is configured" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block error_content %}
|
||||||
|
{% blocktranslate %}It seems there is no station configured for this website:{% endblocktranslate %}
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
{% trans "If you are the website administrator, please connect to administration interface." %}
|
||||||
|
<a href="{% url "admin:login" %}">
|
||||||
|
{% trans "Go to admin" %}
|
||||||
|
<span class="icon"><i class="fa fa-external-link"></i></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{% trans "If you are a visitor, please contact your favorite radio" %}
|
||||||
|
<span class="icon text-danger"><i class="fa fa-heart"></i></span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
|
@ -66,6 +66,8 @@ Context variables:
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if not no_actions %}
|
||||||
{% block actions %}{% endblock %}
|
{% block actions %}{% endblock %}
|
||||||
|
{% endif %}
|
||||||
</article>
|
</article>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
41
aircox/templates/aircox/widgets/comment_item.html
Normal file
41
aircox/templates/aircox/widgets/comment_item.html
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
{% load i18n %}
|
||||||
|
<article class="media item {% block css %}{% endblock%}">
|
||||||
|
<div class="media-content">
|
||||||
|
{% if request.user.is_staff %}
|
||||||
|
<span class="float-right">
|
||||||
|
<a href="{% url "admin:aircox_comment_change" object.pk %}"
|
||||||
|
title="{% trans "Edit comment" %}"
|
||||||
|
aria-label="{% trans "Edit comment" %}">
|
||||||
|
<span class="fa fa-edit"></span>
|
||||||
|
</a>
|
||||||
|
<a class="has-text-danger"
|
||||||
|
title="{% trans "Delete comment" %}"
|
||||||
|
aria-label="{% trans "Delete comment" %}"
|
||||||
|
href="{% url "admin:aircox_comment_delete" object.pk %}">
|
||||||
|
<span class="fa fa-trash-alt"></span>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if with_title %}
|
||||||
|
<h5 class="title is-5 has-text-weight-normal">
|
||||||
|
{% block title %}
|
||||||
|
<a href="{{ object.get_absolute_url }}">{{ object.parent.title }}</a>
|
||||||
|
{% endblock %}
|
||||||
|
</h5>
|
||||||
|
{% endif %}
|
||||||
|
<div class="subtitle is-6 has-text-weight-light">
|
||||||
|
{% block subtitle %}
|
||||||
|
{% if request.user.is_staff %}
|
||||||
|
<a href="mailto:{{ object.email }}">{{ object.nickname }}</a>
|
||||||
|
{% else %}
|
||||||
|
{{ object.nickname }}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
—
|
||||||
|
{{ object.date }}
|
||||||
|
</div>
|
||||||
|
<div class="headline">
|
||||||
|
{% block headline %}{{ object.content }}{% endblock %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
0
aircox/tests/admin/__init__.py
Normal file
0
aircox/tests/admin/__init__.py
Normal file
56
aircox/tests/controllers/test_diffusion_monitor.py
Normal file
56
aircox/tests/controllers/test_diffusion_monitor.py
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
from datetime import date, datetime, timedelta, time
|
||||||
|
from django.utils import timezone as tz
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from aircox import models
|
||||||
|
from aircox.controllers import diffusion_monitor
|
||||||
|
|
||||||
|
now = date.today()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def monitor(logger):
|
||||||
|
logger = logger._imeta.clone().inject(diffusion_monitor, "logger")
|
||||||
|
yield diffusion_monitor.DiffusionMonitor(date=now)
|
||||||
|
logger.release()
|
||||||
|
|
||||||
|
|
||||||
|
class TestDiffusion:
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_update(self, monitor, schedules, sched_initials, logger):
|
||||||
|
monitor.update()
|
||||||
|
|
||||||
|
diffusions = models.Diffusion.objects.filter(
|
||||||
|
schedule__in=sched_initials
|
||||||
|
)
|
||||||
|
by_date = {}
|
||||||
|
for diff in diffusions:
|
||||||
|
assert diff.episode_id
|
||||||
|
by_date.setdefault(diff.schedule_id, set()).add(
|
||||||
|
(diff.start, diff.end)
|
||||||
|
)
|
||||||
|
|
||||||
|
for schedule in sched_initials:
|
||||||
|
if schedule.pk not in by_date:
|
||||||
|
continue
|
||||||
|
_, items = schedule.diffusions_of_month(now)
|
||||||
|
assert all(
|
||||||
|
(item.start, item.end) in by_date[schedule.pk]
|
||||||
|
for item in items
|
||||||
|
)
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_clean(self, monitor, episode):
|
||||||
|
start = tz.make_aware(
|
||||||
|
datetime.combine(monitor.date - timedelta(days=1), time(10, 20))
|
||||||
|
)
|
||||||
|
diff = models.Diffusion(
|
||||||
|
type=models.Diffusion.TYPE_UNCONFIRMED,
|
||||||
|
episode=episode,
|
||||||
|
start=start,
|
||||||
|
end=start + timedelta(minutes=30),
|
||||||
|
)
|
||||||
|
diff.save()
|
||||||
|
monitor.clean()
|
||||||
|
assert not models.Diffusion.objects.filter(pk=diff.pk).first()
|
110
aircox/tests/models/test_signals.py
Normal file
110
aircox/tests/models/test_signals.py
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
from datetime import timedelta
|
||||||
|
from django.contrib.auth.models import User, Group
|
||||||
|
from django.utils import timezone as tz
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from model_bakery import baker
|
||||||
|
|
||||||
|
from aircox.conf import settings
|
||||||
|
from aircox.models import signals, Diffusion, Episode
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sched(program, sched_initials):
|
||||||
|
return next(r for r in sched_initials if r.program == program)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def eps_diffs(program, sched):
|
||||||
|
eps = baker.make(Episode, program=program, _quantity=3)
|
||||||
|
diffs = []
|
||||||
|
for ep in eps:
|
||||||
|
diffs += baker.make(
|
||||||
|
Diffusion,
|
||||||
|
start=tz.now() + timedelta(days=10),
|
||||||
|
end=tz.now() + timedelta(days=10, hours=1),
|
||||||
|
schedule=sched,
|
||||||
|
episode=ep,
|
||||||
|
_quantity=3,
|
||||||
|
)
|
||||||
|
return eps, diffs
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_user_default_groups():
|
||||||
|
user = User(username="test")
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
default_groups = settings.DEFAULT_USER_GROUPS
|
||||||
|
groups = Group.objects.filter(name__in=default_groups.keys())
|
||||||
|
assert groups.exists()
|
||||||
|
assert all(
|
||||||
|
set(group.permissions.all().values_list("codename", flat=True))
|
||||||
|
== set(default_groups[group.name])
|
||||||
|
for group in groups
|
||||||
|
)
|
||||||
|
user_groups = set(user.groups.all().values_list("name", flat=True))
|
||||||
|
assert set(default_groups.keys()) == user_groups
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_user_default_groups_skip_on_superuser():
|
||||||
|
user = User(username="test", is_superuser=True)
|
||||||
|
user.save()
|
||||||
|
assert list(user.groups.all()) == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_page_post_save(program, episodes):
|
||||||
|
episodes = [r for r in episodes if r.program == program]
|
||||||
|
for episode in episodes:
|
||||||
|
episode.cover = None
|
||||||
|
Episode.objects.bulk_update(episodes, ["cover"])
|
||||||
|
# TODO: cover must be an fk to Image
|
||||||
|
# program.cover = "dummy/cover.png"
|
||||||
|
# program.save()
|
||||||
|
|
||||||
|
# query = Episode.objects.filter(program=program) \
|
||||||
|
# .values_list("cover", flat=True)
|
||||||
|
# assert all(r == program.cover for r in query)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_program_post_save(program, eps_diffs):
|
||||||
|
eps, diffs = eps_diffs
|
||||||
|
program.active = False
|
||||||
|
program.save()
|
||||||
|
|
||||||
|
eps_ids = [r.id for r in eps]
|
||||||
|
diff_ids = [r.id for r in diffs]
|
||||||
|
assert not Episode.objects.filter(id__in=eps_ids).exists()
|
||||||
|
assert not Diffusion.objects.filter(id__in=diff_ids).exists()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_schedule_pre_save(sched_initials):
|
||||||
|
sched = sched_initials[0]
|
||||||
|
signals.schedule_pre_save(None, sched)
|
||||||
|
assert getattr(sched, "_initial")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_schedule_post_save():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_schedule_pre_delete(sched, eps_diffs):
|
||||||
|
eps, diffs = eps_diffs
|
||||||
|
signals.schedule_pre_delete(None, sched)
|
||||||
|
assert not Episode.objects.filter(id__in=(r.id for r in eps)).exists()
|
||||||
|
assert not Diffusion.objects.filter(id__in=(r.id for r in diffs)).exists()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_diffusion_post_delete(eps_diffs):
|
||||||
|
eps = eps_diffs[0][0]
|
||||||
|
Diffusion.objects.filter(
|
||||||
|
id__in=[r.id for r in eps.diffusion_set.all()]
|
||||||
|
).delete()
|
||||||
|
assert Episode.objects.filter(id=eps.id).first() is None
|
48
aircox/tests/test_middleware.py
Normal file
48
aircox/tests/test_middleware.py
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import pytest
|
||||||
|
from django.utils import timezone as tz
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from aircox.middleware import AircoxMiddleware
|
||||||
|
|
||||||
|
from .conftest import req_factory
|
||||||
|
|
||||||
|
|
||||||
|
settings.ALLOWED_HOSTS = list(settings.ALLOWED_HOSTS) + ["unknown-host"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def middleware():
|
||||||
|
return AircoxMiddleware(lambda r: r)
|
||||||
|
|
||||||
|
|
||||||
|
class TestAircoxMiddleware:
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_get_station(self, middleware, station, sub_station):
|
||||||
|
req = req_factory.get("/tmp/test", headers={"host": sub_station.hosts})
|
||||||
|
assert middleware.get_station(req) == sub_station
|
||||||
|
|
||||||
|
req = req_factory.get("/tmp/test", headers={"host": station.hosts})
|
||||||
|
assert middleware.get_station(req) == station
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_get_station_use_default(self, middleware, station, stations):
|
||||||
|
req = req_factory.get("/tmp/test", headers={"host": "unknown-host"})
|
||||||
|
assert middleware.get_station(req) == station
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_init_timezone(self, middleware):
|
||||||
|
req = req_factory.get("/tmp/test")
|
||||||
|
req.session = {middleware.timezone_session_key: "Europe/Brussels"}
|
||||||
|
middleware.init_timezone(req)
|
||||||
|
|
||||||
|
current_tz = tz.get_current_timezone()
|
||||||
|
assert current_tz.key == "Europe/Brussels"
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_init_timezone_wrong_timezone(self, middleware):
|
||||||
|
req = req_factory.get("/tmp/test")
|
||||||
|
req.session = {middleware.timezone_session_key: "Oceania/Arlon"}
|
||||||
|
middleware.init_timezone(req)
|
||||||
|
|
||||||
|
current_tz = tz.get_current_timezone()
|
||||||
|
assert current_tz.key != "Oceania/Arlon"
|
45
aircox/tests/test_utils.py
Normal file
45
aircox/tests/test_utils.py
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
from datetime import date, datetime, timedelta
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from aircox import utils
|
||||||
|
|
||||||
|
|
||||||
|
def test_redirect():
|
||||||
|
with pytest.raises(utils.Redirect):
|
||||||
|
utils.redirect("/redirect")
|
||||||
|
|
||||||
|
|
||||||
|
def test_str_to_date():
|
||||||
|
result = utils.str_to_date("2023-01-10", "-")
|
||||||
|
assert result == date(2023, 1, 10)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cast_date():
|
||||||
|
val = datetime(2023, 1, 12)
|
||||||
|
result = utils.cast_date(val)
|
||||||
|
assert isinstance(result, date)
|
||||||
|
assert result == val.date()
|
||||||
|
|
||||||
|
|
||||||
|
def test_date_or_default():
|
||||||
|
result = utils.date_or_default(None, date)
|
||||||
|
assert isinstance(result, date)
|
||||||
|
assert result == date.today()
|
||||||
|
|
||||||
|
|
||||||
|
def test_to_timedelta():
|
||||||
|
val = datetime(2023, 1, 10, hour=20, minute=10, second=1)
|
||||||
|
assert utils.to_timedelta(val) == timedelta(
|
||||||
|
hours=20, minutes=10, seconds=1
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_to_seconds():
|
||||||
|
val = datetime(2023, 1, 10, hour=20, minute=10, second=1)
|
||||||
|
assert utils.to_seconds(val) == 20 * 3600 + 10 * 60 + 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_seconds_to_time():
|
||||||
|
val = 20 * 3600 + 10 * 60 + 1
|
||||||
|
result = utils.seconds_to_time(val)
|
||||||
|
assert (result.hour, result.minute, result.second) == (20, 10, 1)
|
0
aircox/tests/views/__init__.py
Normal file
0
aircox/tests/views/__init__.py
Normal file
40
aircox/tests/views/conftest.py
Normal file
40
aircox/tests/views/conftest.py
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import pytest
|
||||||
|
from model_bakery import baker
|
||||||
|
|
||||||
|
from aircox import models
|
||||||
|
|
||||||
|
|
||||||
|
class FakeView:
|
||||||
|
context = None
|
||||||
|
kwargs = {}
|
||||||
|
|
||||||
|
def ___init__(self):
|
||||||
|
self.kwargs = {}
|
||||||
|
|
||||||
|
def get(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.queryset
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def published_pages():
|
||||||
|
return baker.make(
|
||||||
|
models.Page, status=models.StaticPage.STATUS_PUBLISHED, _quantity=3
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def unpublished_pages():
|
||||||
|
return baker.make(
|
||||||
|
models.Page, status=models.StaticPage.STATUS_DRAFT, _quantity=3
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def pages(published_pages, unpublished_pages):
|
||||||
|
return published_pages + unpublished_pages
|
76
aircox/tests/views/test_base.py
Normal file
76
aircox/tests/views/test_base.py
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from aircox import models
|
||||||
|
from aircox.test import Interface
|
||||||
|
from aircox.views import base
|
||||||
|
from .conftest import FakeView
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def fake_request(station):
|
||||||
|
return Interface(station=station)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def base_view(fake_request):
|
||||||
|
class View(base.BaseView, FakeView):
|
||||||
|
model = models.Page
|
||||||
|
request = fake_request
|
||||||
|
|
||||||
|
return View()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def base_api_view(fake_request):
|
||||||
|
class View(base.BaseAPIView, FakeView):
|
||||||
|
model = models.Program
|
||||||
|
queryset = models.Program.objects.all()
|
||||||
|
request = fake_request
|
||||||
|
|
||||||
|
return View()
|
||||||
|
|
||||||
|
|
||||||
|
class TestBaseView:
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_station(self, base_view, station):
|
||||||
|
assert base_view.station == station
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_get_sidebar_queryset(self, base_view, pages, published_pages):
|
||||||
|
query = base_view.get_sidebar_queryset().values_list("id", flat=True)
|
||||||
|
page_ids = {r.id for r in published_pages}
|
||||||
|
assert set(query) == page_ids
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_get_sidebar_url(self, base_view):
|
||||||
|
assert base_view.get_sidebar_url() == reverse("page-list")
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_get_context_data(self, base_view, station, published_pages):
|
||||||
|
base_view.has_sidebar = True
|
||||||
|
base_view.get_sidebar_queryset = lambda: published_pages
|
||||||
|
context = base_view.get_context_data()
|
||||||
|
assert context == {
|
||||||
|
"view": base_view,
|
||||||
|
"station": station,
|
||||||
|
"page": None, # get_page() returns None
|
||||||
|
"has_sidebar": base_view.has_sidebar,
|
||||||
|
"has_filters": False,
|
||||||
|
"sidebar_object_list": published_pages[: base_view.list_count],
|
||||||
|
"sidebar_list_url": base_view.get_sidebar_url(),
|
||||||
|
"audio_streams": station.streams,
|
||||||
|
"model": base_view.model,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestBaseAPIView:
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_station(self, base_api_view, station):
|
||||||
|
assert base_api_view.station == station
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_get_queryset(self, base_api_view, station, programs):
|
||||||
|
query = base_api_view.get_queryset()
|
||||||
|
assert set(query.values_list("station", flat=True)) == {station.id}
|
166
aircox/tests/views/test_mixins.py
Normal file
166
aircox/tests/views/test_mixins.py
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from django.http import Http404
|
||||||
|
from model_bakery import baker
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from aircox import filters, models
|
||||||
|
from aircox.views import mixins
|
||||||
|
from aircox.test import Interface
|
||||||
|
from aircox.tests.conftest import req_factory
|
||||||
|
from .conftest import FakeView
|
||||||
|
|
||||||
|
|
||||||
|
today = date.today()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def redirect_interface():
|
||||||
|
iface = Interface.inject(mixins, "redirect", {"__call__": "redirect"})
|
||||||
|
yield iface
|
||||||
|
iface._irelease()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def date_mixin():
|
||||||
|
class Mixin(mixins.GetDateMixin, FakeView):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return Mixin()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def parent_mixin():
|
||||||
|
class Mixin(mixins.ParentMixin, FakeView):
|
||||||
|
parent_model = models.Program
|
||||||
|
|
||||||
|
return Mixin()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def attach_mixin():
|
||||||
|
class Mixin(mixins.AttachedToMixin, FakeView):
|
||||||
|
attach_to_value = models.StaticPage.ATTACH_TO_HOME
|
||||||
|
|
||||||
|
return Mixin()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def filters_mixin():
|
||||||
|
class Mixin(mixins.FiltersMixin, FakeView):
|
||||||
|
filterset_class = filters.PageFilters
|
||||||
|
queryset = models.Page.objects.all()
|
||||||
|
|
||||||
|
return Mixin()
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetDateMixin:
|
||||||
|
req = req_factory.get("/test", {"date": today.strftime("%Y-%m-%d")})
|
||||||
|
|
||||||
|
def test_get_date(self, date_mixin):
|
||||||
|
date_mixin.request = self.req
|
||||||
|
assert date_mixin.get_date() == today
|
||||||
|
|
||||||
|
def test_get_date_from_kwargs(self, date_mixin):
|
||||||
|
date_mixin.request = req_factory.get("/test")
|
||||||
|
date_mixin.kwargs = {"date": today}
|
||||||
|
assert date_mixin.get_date() == today
|
||||||
|
|
||||||
|
def test_get_date_none_provided(self, date_mixin):
|
||||||
|
date_mixin.request = req_factory.get("/test")
|
||||||
|
assert date_mixin.get_date() is None
|
||||||
|
|
||||||
|
def test_get_redirect(self, date_mixin, redirect_interface):
|
||||||
|
date_mixin.redirect_date_url = "redirect_date_url"
|
||||||
|
date_mixin.request = self.req
|
||||||
|
assert date_mixin.get() == "redirect"
|
||||||
|
assert redirect_interface._trace() == (
|
||||||
|
(date_mixin.redirect_date_url,),
|
||||||
|
{"date": today.strftime("%Y/%m/%d")},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_get_calls_get_date(self, date_mixin):
|
||||||
|
date_mixin.get_date = lambda: today
|
||||||
|
date_mixin.get()
|
||||||
|
assert date_mixin.date == today
|
||||||
|
|
||||||
|
|
||||||
|
class TestParentMixin:
|
||||||
|
req = req_factory.get("/test")
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_get_parent(self, parent_mixin, program):
|
||||||
|
parent = parent_mixin.get_parent(self.req, parent_slug=program.slug)
|
||||||
|
assert parent.pk == program.pk
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_get_parent_raises_404(self, parent_mixin):
|
||||||
|
with pytest.raises(Http404):
|
||||||
|
parent_mixin.get_parent(
|
||||||
|
self.req, parent_slug="parent-invalid-slug"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_get_parent_not_parent_model(self, parent_mixin):
|
||||||
|
parent_mixin.parent_model = None
|
||||||
|
assert parent_mixin.get_parent(self.req) is None
|
||||||
|
|
||||||
|
def test_get_parent_not_parent_url_kwargs(self, parent_mixin):
|
||||||
|
assert parent_mixin.get_parent(self.req) is None
|
||||||
|
|
||||||
|
def test_get_calls_parent(self, parent_mixin):
|
||||||
|
parent = "parent object"
|
||||||
|
parent_mixin.get_parent = lambda *_, **kw: parent
|
||||||
|
parent_mixin.get(self.req)
|
||||||
|
assert parent_mixin.parent == parent
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_get_queryset_with_parent(self, parent_mixin, program, episodes):
|
||||||
|
parent_mixin.queryset = models.Episode.objects.all()
|
||||||
|
parent_mixin.parent = program
|
||||||
|
episodes_id = {r.id for r in episodes if r.parent_id == program.id}
|
||||||
|
query = parent_mixin.get_queryset().values_list("id", flat=True)
|
||||||
|
assert set(query) == episodes_id
|
||||||
|
|
||||||
|
def test_get_context_data_with_parent(self, parent_mixin):
|
||||||
|
parent_mixin.parent = Interface(cover="parent-cover")
|
||||||
|
context = parent_mixin.get_context_data()
|
||||||
|
assert context["cover"] == "parent-cover"
|
||||||
|
|
||||||
|
|
||||||
|
class TestAttachedToMixin:
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_get_page_with_attach_to(self, attach_mixin):
|
||||||
|
page = baker.make(
|
||||||
|
models.StaticPage,
|
||||||
|
attach_to=attach_mixin.attach_to_value,
|
||||||
|
status=models.StaticPage.STATUS_PUBLISHED,
|
||||||
|
)
|
||||||
|
assert attach_mixin.get_page() == page
|
||||||
|
|
||||||
|
|
||||||
|
class TestFiltersMixin:
|
||||||
|
req = req_factory.get("/test", {"data": True, "page": "page"})
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_get_filterset(self, filters_mixin):
|
||||||
|
filterset = filters_mixin.get_filterset({}, models.Page.objects.all())
|
||||||
|
assert isinstance(filterset, filters_mixin.filterset_class)
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_get_queryset(self, filters_mixin):
|
||||||
|
filterset = Interface(qs="filterset-qs")
|
||||||
|
filters_mixin.request = self.req
|
||||||
|
filters_mixin.get_filterset = lambda *_, **__: filterset
|
||||||
|
assert filters_mixin.get_queryset() == filterset.qs
|
||||||
|
|
||||||
|
def test_get_context_data_valid_filterset(self, filters_mixin):
|
||||||
|
filterset = Interface(
|
||||||
|
None,
|
||||||
|
{"is_valid": True},
|
||||||
|
qs="filterset-qs",
|
||||||
|
form=Interface(cleaned_data="cleaned_data"),
|
||||||
|
)
|
||||||
|
filters_mixin.request = self.req
|
||||||
|
context = filters_mixin.get_context_data(filterset=filterset)
|
||||||
|
assert context["filterset_data"] == "cleaned_data"
|
||||||
|
assert dict(context["get_params"]) == {"data": ["True"]}
|
|
@ -109,4 +109,9 @@ urls = [
|
||||||
views.ProgramPageListView.as_view(),
|
views.ProgramPageListView.as_view(),
|
||||||
name="program-page-list",
|
name="program-page-list",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"errors/no-station",
|
||||||
|
views.errors.NoStationErrorView.as_view(),
|
||||||
|
name="errors-no-station",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from . import admin
|
from . import admin, errors
|
||||||
from .article import ArticleDetailView, ArticleListView
|
from .article import ArticleDetailView, ArticleListView
|
||||||
from .base import BaseAPIView, BaseView
|
from .base import BaseAPIView, BaseView
|
||||||
from .diffusion import DiffusionListView
|
from .diffusion import DiffusionListView
|
||||||
|
@ -20,6 +20,7 @@ from .program import (
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
"admin",
|
"admin",
|
||||||
|
"errors",
|
||||||
"ArticleDetailView",
|
"ArticleDetailView",
|
||||||
"ArticleListView",
|
"ArticleListView",
|
||||||
"BaseAPIView",
|
"BaseAPIView",
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.views.generic.base import ContextMixin, TemplateResponseMixin
|
from django.views.generic.base import ContextMixin, TemplateResponseMixin
|
||||||
|
|
||||||
|
@ -60,6 +61,11 @@ class BaseView(TemplateResponseMixin, ContextMixin):
|
||||||
|
|
||||||
return super().get_context_data(**kwargs)
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
def dispatch(self, *args, **kwargs):
|
||||||
|
if not self.request.station:
|
||||||
|
return HttpResponseRedirect(reverse("errors-no-station"))
|
||||||
|
return super().dispatch(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
# FIXME: rename to sth like [Base]?StationAPIView/Mixin
|
# FIXME: rename to sth like [Base]?StationAPIView/Mixin
|
||||||
class BaseAPIView:
|
class BaseAPIView:
|
||||||
|
|
8
aircox/views/errors.py
Normal file
8
aircox/views/errors.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
from django.views.generic.base import TemplateView
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ("NoStationErrorView",)
|
||||||
|
|
||||||
|
|
||||||
|
class NoStationErrorView(TemplateView):
|
||||||
|
template_name = "aircox/errors/no_station.html"
|
Binary file not shown.
|
@ -8,7 +8,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2022-05-21 14:30+0000\n"
|
"POT-Creation-Date: 2023-09-12 18:48+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
@ -18,18 +18,6 @@ msgstr ""
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||||
|
|
||||||
#: aircox_streamer/controllers.py:75
|
|
||||||
msgid "playing"
|
|
||||||
msgstr "en cours de lecture"
|
|
||||||
|
|
||||||
#: aircox_streamer/controllers.py:77
|
|
||||||
msgid "paused"
|
|
||||||
msgstr "pause"
|
|
||||||
|
|
||||||
#: aircox_streamer/controllers.py:79
|
|
||||||
msgid "stopped"
|
|
||||||
msgstr "arrêt"
|
|
||||||
|
|
||||||
#: aircox_streamer/templates/aircox_streamer/source_item.html:18
|
#: aircox_streamer/templates/aircox_streamer/source_item.html:18
|
||||||
msgid "Edit related program"
|
msgid "Edit related program"
|
||||||
msgstr "Éditer le programme correspondant"
|
msgstr "Éditer le programme correspondant"
|
||||||
|
@ -103,6 +91,15 @@ msgstr "Recharger"
|
||||||
msgid "Select a station"
|
msgid "Select a station"
|
||||||
msgstr "Sélectionner une station"
|
msgstr "Sélectionner une station"
|
||||||
|
|
||||||
#: aircox_streamer/urls.py:10 aircox_streamer/views.py:9
|
#: aircox_streamer/urls.py:13 aircox_streamer/views.py:10
|
||||||
msgid "Streamer Monitor"
|
msgid "Streamer Monitor"
|
||||||
msgstr "Moniteur de stream"
|
msgstr "Moniteur de stream"
|
||||||
|
|
||||||
|
#~ msgid "playing"
|
||||||
|
#~ msgstr "en cours de lecture"
|
||||||
|
|
||||||
|
#~ msgid "paused"
|
||||||
|
#~ msgstr "pause"
|
||||||
|
|
||||||
|
#~ msgid "stopped"
|
||||||
|
#~ msgstr "arrêt"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user