feat: add error message page; improve admin ui; add missing test files
This commit is contained in:
		@ -3,7 +3,7 @@ from django.urls import include, path, reverse
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from rest_framework.routers import DefaultRouter
 | 
			
		||||
 | 
			
		||||
from .models import Comment, Diffusion, Program
 | 
			
		||||
from . import models
 | 
			
		||||
from .views.admin import StatisticsView
 | 
			
		||||
 | 
			
		||||
__all__ = ["AdminSite"]
 | 
			
		||||
@ -26,17 +26,20 @@ class AdminSite(admin.AdminSite):
 | 
			
		||||
        context.update(
 | 
			
		||||
            {
 | 
			
		||||
                # all programs
 | 
			
		||||
                "programs": Program.objects.active()
 | 
			
		||||
                "programs": models.Program.objects.active()
 | 
			
		||||
                .values("pk", "title")
 | 
			
		||||
                .order_by("title"),
 | 
			
		||||
                # today's diffusions
 | 
			
		||||
                "diffusions": Diffusion.objects.date()
 | 
			
		||||
                "diffusions": models.Diffusion.objects.date()
 | 
			
		||||
                .order_by("start")
 | 
			
		||||
                .select_related("episode"),
 | 
			
		||||
                # TODO: only for dashboard
 | 
			
		||||
                # last comments
 | 
			
		||||
                "comments": Comment.objects.order_by("-date").select_related(
 | 
			
		||||
                    "page"
 | 
			
		||||
                "comments": models.Comment.objects.order_by(
 | 
			
		||||
                    "-date"
 | 
			
		||||
                ).select_related("page")[0:10],
 | 
			
		||||
                "latests": models.Page.objects.select_subclasses().order_by(
 | 
			
		||||
                    "-pub_date"
 | 
			
		||||
                )[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.utils import timezone as tz
 | 
			
		||||
 | 
			
		||||
from aircox.controllers.diffusions import Diffusions
 | 
			
		||||
from aircox.controllers.diffusion_monitor import DiffusionMonitor
 | 
			
		||||
 | 
			
		||||
logger = logging.getLogger("aircox.commands")
 | 
			
		||||
 | 
			
		||||
@ -70,7 +70,7 @@ class Command(BaseCommand):
 | 
			
		||||
                date += tz.timedelta(days=28)
 | 
			
		||||
            date = date.replace(day=1)
 | 
			
		||||
 | 
			
		||||
        actions = Diffusions(date)
 | 
			
		||||
        actions = DiffusionMonitor(date)
 | 
			
		||||
        if options.get("update"):
 | 
			
		||||
            actions.update()
 | 
			
		||||
        if options.get("clean"):
 | 
			
		||||
 | 
			
		||||
@ -278,6 +278,16 @@ class Comment(models.Model):
 | 
			
		||||
    date = models.DateTimeField(auto_now_add=True)
 | 
			
		||||
    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:
 | 
			
		||||
        verbose_name = _("Comment")
 | 
			
		||||
        verbose_name_plural = _("Comments")
 | 
			
		||||
 | 
			
		||||
@ -65,7 +65,10 @@
 | 
			
		||||
            <div class="navbar-start">
 | 
			
		||||
                {# Today's diffusions #}
 | 
			
		||||
                <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">
 | 
			
		||||
                        {% 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 %}">
 | 
			
		||||
@ -76,23 +79,12 @@
 | 
			
		||||
                    </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 #}
 | 
			
		||||
                <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">
 | 
			
		||||
                        <input type="text" onkeyup="aircox.filter_menu(event)"
 | 
			
		||||
                               placeholder="{% translate "Search" %}" class="navbar-item input" />
 | 
			
		||||
@ -104,9 +96,29 @@
 | 
			
		||||
                    </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 #}
 | 
			
		||||
                <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">
 | 
			
		||||
                        <input type="text" onkeyup="aircox.filter_menu(event)"
 | 
			
		||||
                               placeholder="{% translate "Search" %}" class="navbar-item input" />
 | 
			
		||||
@ -121,7 +133,10 @@
 | 
			
		||||
 | 
			
		||||
            <div class="navbar-end">
 | 
			
		||||
                <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">
 | 
			
		||||
                    {% get_admin_tools as admin_tools %}
 | 
			
		||||
                    {% for label, url in admin_tools %}
 | 
			
		||||
@ -131,8 +146,9 @@
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="navbar-item has-dropdown is-hoverable">
 | 
			
		||||
                    <a href="{% url "admin:auth_user_change" user.pk %}" class="navbar-link">
 | 
			
		||||
                        {% firstof user.get_short_name user.get_username %}
 | 
			
		||||
                    <a href="{% url "admin:auth_user_change" user.pk %}" class="icon-text navbar-link">
 | 
			
		||||
                        <span class="icon"><i class="fa fa-user"></i></span>
 | 
			
		||||
                        <span>{% firstof user.get_short_name user.get_username %}</span>
 | 
			
		||||
                    </a>
 | 
			
		||||
                    <div class="navbar-dropdown is-boxed is-right">
 | 
			
		||||
                        {% block userlinks %}
 | 
			
		||||
@ -179,6 +195,7 @@
 | 
			
		||||
 | 
			
		||||
    <!-- Content -->
 | 
			
		||||
    <div id="app">
 | 
			
		||||
        {% block app %}
 | 
			
		||||
        <div id="content" class="{% block coltype %}colM{% endblock %}">
 | 
			
		||||
            {% block pretitle %}{% endblock %}
 | 
			
		||||
            {% block content_title %}{% if title %}<h1 class="title is-3">{{ title }}</h1>{% endif %}{% endblock %}
 | 
			
		||||
@ -189,6 +206,7 @@
 | 
			
		||||
            {% block sidebar %}{% endblock %}
 | 
			
		||||
            <br class="clear">
 | 
			
		||||
        </div>
 | 
			
		||||
        {% endblock %}
 | 
			
		||||
    </div>
 | 
			
		||||
    <!-- END Content -->
 | 
			
		||||
 | 
			
		||||
@ -197,7 +215,9 @@
 | 
			
		||||
<!-- END Container -->
 | 
			
		||||
 | 
			
		||||
{% block player %}
 | 
			
		||||
{% if request.station %}
 | 
			
		||||
<div id="player">{% include "aircox/widgets/player.html" %}</div>
 | 
			
		||||
{% endif %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
</body>
 | 
			
		||||
 | 
			
		||||
@ -2,13 +2,16 @@
 | 
			
		||||
{% load i18n thumbnail %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{% block messages %}
 | 
			
		||||
{{ block.super }}
 | 
			
		||||
 | 
			
		||||
{% block app %}
 | 
			
		||||
<div class="section">
 | 
			
		||||
<div class="columns">
 | 
			
		||||
    <div class="column">
 | 
			
		||||
        <h1 class="title is-4">{% translate "Today" %}</h1>
 | 
			
		||||
        <div class="box">
 | 
			
		||||
            <h1 class="title icon-text is-4">
 | 
			
		||||
                <span class="icon"><i class="fa-regular fa-calendar-days"></i></span>
 | 
			
		||||
                <span>{% translate "Today" %}</span>
 | 
			
		||||
            </h1>
 | 
			
		||||
            {% if diffusions %}
 | 
			
		||||
            <table class="table is-fullwidth is-striped">
 | 
			
		||||
                <tbody>
 | 
			
		||||
                    {% for diffusion in diffusions %}
 | 
			
		||||
@ -46,48 +49,45 @@
 | 
			
		||||
                    {% endfor %}
 | 
			
		||||
                </tbody>
 | 
			
		||||
            </table>
 | 
			
		||||
            {% else %}
 | 
			
		||||
            <div class="block has-text-centered">
 | 
			
		||||
                {% trans "No diffusion is scheduled for today." %}
 | 
			
		||||
            </div>
 | 
			
		||||
    <div class="column">
 | 
			
		||||
        <h1 class="title is-4">{% translate "Latest comments" %}</h1>
 | 
			
		||||
        <table class="table is-fullwidth is-striped">
 | 
			
		||||
        {% for comment in comments %}
 | 
			
		||||
        {% with page=comment.page %}
 | 
			
		||||
        <tr>
 | 
			
		||||
            <th>
 | 
			
		||||
                {{ page.title }}
 | 
			
		||||
                </a>
 | 
			
		||||
                |
 | 
			
		||||
                <span title="{{ comment.email }}">{{ comment.nickname }}</span>
 | 
			
		||||
                —
 | 
			
		||||
                <span>{{ comment.date }}</span>
 | 
			
		||||
                <span class="float-right">
 | 
			
		||||
                    <a href="{% url "admin:aircox_comment_change" comment.pk %}"
 | 
			
		||||
                       title="{% translate "Edit comment" %}"
 | 
			
		||||
                       aria-label="{% translate "Edit comment" %}">
 | 
			
		||||
                        <span class="fa fa-edit"></span>
 | 
			
		||||
                    </a>
 | 
			
		||||
                    <a class="has-text-danger"
 | 
			
		||||
                       title="{% translate "Delete comment" %}"
 | 
			
		||||
                       aria-label="{% translate "Delete comment" %}"
 | 
			
		||||
                       href="{% url "admin:aircox_comment_delete" comment.pk %}">
 | 
			
		||||
                        <span class="fa fa-trash-alt"></span>
 | 
			
		||||
                    </a>
 | 
			
		||||
            </th>
 | 
			
		||||
        </tr>
 | 
			
		||||
        <tr>
 | 
			
		||||
            <td colspan="2">
 | 
			
		||||
                {{ comment.content|slice:"0:128" }}
 | 
			
		||||
            </td>
 | 
			
		||||
        </tr>
 | 
			
		||||
        {% endwith %}
 | 
			
		||||
        {% endfor %}
 | 
			
		||||
            </tbody>
 | 
			
		||||
        </table>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="box">
 | 
			
		||||
            <h1 class="title is-4 icon-text">
 | 
			
		||||
                <span class="icon"><i class="fa-regular fa-comments"></i></span>
 | 
			
		||||
                <span>{% translate "Latest comments" %}</span>
 | 
			
		||||
            </h1>
 | 
			
		||||
            {% if comments %}
 | 
			
		||||
            {% include "aircox/widgets/page_list.html" with object_list=comments with_title=True %}
 | 
			
		||||
            <div class="has-text-centered">
 | 
			
		||||
                <a href="{% url "admin:aircox_comment_changelist" %}" class="float-center">{% translate "All comments" %}</a>
 | 
			
		||||
            </div>
 | 
			
		||||
            {% else %}
 | 
			
		||||
            <p class="block has-text-centered">{% trans "No comment posted yet" %}</p>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <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>
 | 
			
		||||
 | 
			
		||||
@ -48,6 +48,7 @@ Usefull context:
 | 
			
		||||
            })
 | 
			
		||||
        </script>
 | 
			
		||||
        <div id="app">
 | 
			
		||||
            {% block top-nav-container %}
 | 
			
		||||
            <nav class="navbar has-shadow" role="navigation" aria-label="main navigation">
 | 
			
		||||
                <div class="container">
 | 
			
		||||
                    <div class="navbar-brand">
 | 
			
		||||
@ -84,6 +85,7 @@ Usefull context:
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </nav>
 | 
			
		||||
            {% endblock %}
 | 
			
		||||
 | 
			
		||||
            <div class="container">
 | 
			
		||||
                <div class="columns is-desktop">
 | 
			
		||||
@ -161,6 +163,8 @@ Usefull context:
 | 
			
		||||
 | 
			
		||||
            <hr>
 | 
			
		||||
        </div>
 | 
			
		||||
        {% block player-container %}
 | 
			
		||||
        <div id="player">{% include "aircox/widgets/player.html" %}</div>
 | 
			
		||||
        {% endblock %}
 | 
			
		||||
    </body>
 | 
			
		||||
</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 %}
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    {% if not no_actions %}
 | 
			
		||||
    {% block actions %}{% endblock %}
 | 
			
		||||
    {% endif %}
 | 
			
		||||
</article>
 | 
			
		||||
{% 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(),
 | 
			
		||||
        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 .base import BaseAPIView, BaseView
 | 
			
		||||
from .diffusion import DiffusionListView
 | 
			
		||||
@ -20,6 +20,7 @@ from .program import (
 | 
			
		||||
 | 
			
		||||
__all__ = (
 | 
			
		||||
    "admin",
 | 
			
		||||
    "errors",
 | 
			
		||||
    "ArticleDetailView",
 | 
			
		||||
    "ArticleListView",
 | 
			
		||||
    "BaseAPIView",
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
from django.http import HttpResponseRedirect
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
from django.views.generic.base import ContextMixin, TemplateResponseMixin
 | 
			
		||||
 | 
			
		||||
@ -60,6 +61,11 @@ class BaseView(TemplateResponseMixin, ContextMixin):
 | 
			
		||||
 | 
			
		||||
        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
 | 
			
		||||
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 ""
 | 
			
		||||
"Project-Id-Version: PACKAGE VERSION\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"
 | 
			
		||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 | 
			
		||||
"Language-Team: LANGUAGE <LL@li.org>\n"
 | 
			
		||||
@ -18,18 +18,6 @@ msgstr ""
 | 
			
		||||
"Content-Transfer-Encoding: 8bit\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
 | 
			
		||||
msgid "Edit related program"
 | 
			
		||||
msgstr "Éditer le programme correspondant"
 | 
			
		||||
@ -103,6 +91,15 @@ msgstr "Recharger"
 | 
			
		||||
msgid "Select a 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"
 | 
			
		||||
msgstr "Moniteur de stream"
 | 
			
		||||
 | 
			
		||||
#~ msgid "playing"
 | 
			
		||||
#~ msgstr "en cours de lecture"
 | 
			
		||||
 | 
			
		||||
#~ msgid "paused"
 | 
			
		||||
#~ msgstr "pause"
 | 
			
		||||
 | 
			
		||||
#~ msgid "stopped"
 | 
			
		||||
#~ msgstr "arrêt"
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user