Compare commits
	
		
			21 Commits
		
	
	
		
			1aababe2ae
			...
			9378435345
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 9378435345 | |||
| 07075d3b90 | |||
| b46ae584a2 | |||
| 85cfe43cb1 | |||
| 042d3d65aa | |||
| 49f1aee9fc | |||
| 6417ecc628 | |||
| 9e1c4277a6 | |||
| 7b5a37894a | |||
| 6e6eb25c96 | |||
| 51db7ba5ee | |||
| fff73235cd | |||
| d68ba9a59e | |||
| ac05d1b09a | |||
| ac6b6b4f79 | |||
| f2f493cac5 | |||
| 1dcdb382b0 | |||
| affe4cee02 | |||
| 972b574299 | |||
| 08f3a9db07 | |||
| 12a9beecfd | 
@ -86,8 +86,8 @@ class Settings(BaseSettings):
 | 
			
		||||
            # TODO include content_type in order to avoid clash with potential
 | 
			
		||||
            #      extra applications
 | 
			
		||||
            # aircox
 | 
			
		||||
            "change_program",
 | 
			
		||||
            "change_episode",
 | 
			
		||||
            "view_program",
 | 
			
		||||
            "view_episode",
 | 
			
		||||
            "change_diffusion",
 | 
			
		||||
            "add_comment",
 | 
			
		||||
            "change_comment",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										4
									
								
								aircox/context_processors/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								aircox/context_processors/__init__.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
			
		||||
def station(request):
 | 
			
		||||
    station = request.station
 | 
			
		||||
    audio_streams = station.streams if station else None
 | 
			
		||||
    return {"station": station, "audio_streams": audio_streams}
 | 
			
		||||
							
								
								
									
										25
									
								
								aircox/migrations/0015_program_editors.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								aircox/migrations/0015_program_editors.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
			
		||||
# Generated by Django 4.2.5 on 2023-10-18 13:50
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("auth", "0012_alter_user_first_name_max_length"),
 | 
			
		||||
        ("aircox", "0014_alter_schedule_timezone"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="program",
 | 
			
		||||
            name="editors",
 | 
			
		||||
            field=models.ForeignKey(
 | 
			
		||||
                blank=True,
 | 
			
		||||
                null=True,
 | 
			
		||||
                on_delete=django.db.models.deletion.CASCADE,
 | 
			
		||||
                to="auth.group",
 | 
			
		||||
                verbose_name="editors",
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										12
									
								
								aircox/migrations/0019_merge_20240119_1022.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								aircox/migrations/0019_merge_20240119_1022.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
			
		||||
# Generated by Django 4.2.7 on 2024-01-19 09:22
 | 
			
		||||
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("aircox", "0015_program_editors"),
 | 
			
		||||
        ("aircox", "0018_alter_staticpage_attach_to"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = []
 | 
			
		||||
@ -3,6 +3,8 @@ import os
 | 
			
		||||
import shutil
 | 
			
		||||
 | 
			
		||||
from django.conf import settings as conf
 | 
			
		||||
from django.contrib.auth.models import Group, Permission
 | 
			
		||||
from django.contrib.contenttypes.models import ContentType
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models import F
 | 
			
		||||
from django.db.models.functions import Concat, Substr
 | 
			
		||||
@ -58,6 +60,7 @@ class Program(Page):
 | 
			
		||||
        default=True,
 | 
			
		||||
        help_text=_("update later diffusions according to schedule changes"),
 | 
			
		||||
    )
 | 
			
		||||
    editors = models.ForeignKey(Group, models.CASCADE, blank=True, null=True, verbose_name=_("editors"))
 | 
			
		||||
 | 
			
		||||
    objects = ProgramQuerySet.as_manager()
 | 
			
		||||
    detail_url_name = "program-detail"
 | 
			
		||||
@ -81,6 +84,14 @@ class Program(Page):
 | 
			
		||||
    def excerpts_path(self):
 | 
			
		||||
        return os.path.join(self.path, settings.SOUND_ARCHIVES_SUBDIR)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def editors_group_name(self):
 | 
			
		||||
        return f"{self.title} editors"
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def change_permission_codename(self):
 | 
			
		||||
        return f"change_program_{self.slug}"
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *kargs, **kwargs):
 | 
			
		||||
        super().__init__(*kargs, **kwargs)
 | 
			
		||||
        if self.slug:
 | 
			
		||||
@ -110,6 +121,18 @@ class Program(Page):
 | 
			
		||||
        os.makedirs(path, exist_ok=True)
 | 
			
		||||
        return os.path.exists(path)
 | 
			
		||||
 | 
			
		||||
    def set_group_ownership(self):
 | 
			
		||||
        editors, created = Group.objects.get_or_create(name=self.editors_group_name)
 | 
			
		||||
        if created:
 | 
			
		||||
            self.editors = editors
 | 
			
		||||
        permission, _ = Permission.objects.get_or_create(
 | 
			
		||||
            name=f"change program {self.title}",
 | 
			
		||||
            codename=self.change_permission_codename,
 | 
			
		||||
            content_type=ContentType.objects.get_for_model(self),
 | 
			
		||||
        )
 | 
			
		||||
        if permission not in editors.permissions.all():
 | 
			
		||||
            editors.permissions.add(permission)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = _("Program")
 | 
			
		||||
        verbose_name_plural = _("Programs")
 | 
			
		||||
@ -135,6 +158,9 @@ class Program(Page):
 | 
			
		||||
            shutil.move(abspath, self.abspath)
 | 
			
		||||
            Sound.objects.filter(path__startswith=path_).update(file=Concat("file", Substr(F("file"), len(path_))))
 | 
			
		||||
 | 
			
		||||
        self.set_group_ownership()
 | 
			
		||||
        super().save(*kargs, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ProgramChildQuerySet(PageQuerySet):
 | 
			
		||||
    def station(self, station=None, id=None):
 | 
			
		||||
 | 
			
		||||
@ -41,8 +41,7 @@ def user_default_groups(sender, instance, created, *args, **kwargs):
 | 
			
		||||
 | 
			
		||||
@receiver(signals.post_save, sender=Page)
 | 
			
		||||
def page_post_save(sender, instance, created, *args, **kwargs):
 | 
			
		||||
    return
 | 
			
		||||
    if not created and instance.cover:
 | 
			
		||||
    if not created and instance.cover and "raw" not in kwargs:
 | 
			
		||||
        Page.objects.filter(parent=instance, cover__isnull=True).update(cover=instance.cover)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -60,8 +59,7 @@ def program_post_save(sender, instance, created, *args, **kwargs):
 | 
			
		||||
 | 
			
		||||
@receiver(signals.pre_save, sender=Schedule)
 | 
			
		||||
def schedule_pre_save(sender, instance, *args, **kwargs):
 | 
			
		||||
    return
 | 
			
		||||
    if getattr(instance, "pk") is not None:
 | 
			
		||||
    if getattr(instance, "pk") is not None and "raw" not in kwargs:
 | 
			
		||||
        instance._initial = Schedule.objects.get(pk=instance.pk)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										34
									
								
								aircox/templates/accounts/profile.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								aircox/templates/accounts/profile.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,34 @@
 | 
			
		||||
{% extends "aircox/base.html" %}
 | 
			
		||||
{% load i18n aircox %}
 | 
			
		||||
 | 
			
		||||
{% block head_title %}
 | 
			
		||||
    {% block title %}{{ user.username }}{% endblock %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content-container %}
 | 
			
		||||
<div class="container content page-content">
 | 
			
		||||
  <h2 class="subtitle">Mon Profil</h2>
 | 
			
		||||
  {% translate "Username" %} : {{ user.username|title }}<br/>
 | 
			
		||||
  <!-- Connexion: {{ user.last_login }} -->
 | 
			
		||||
 | 
			
		||||
  <h2 class="subtitle is-1">Mes émissions</h2>
 | 
			
		||||
  {% if programs|length %}
 | 
			
		||||
  <ul>
 | 
			
		||||
  {% for p in programs %}
 | 
			
		||||
  <li>{{ p.title }} :
 | 
			
		||||
       
 | 
			
		||||
      <a href="{% url 'program-detail' slug=p.slug %}">
 | 
			
		||||
          <span title="{% translate 'View' %} {{ page }}">{% translate 'View' %}</span>
 | 
			
		||||
      </a>
 | 
			
		||||
       
 | 
			
		||||
      <a href="{% url 'program-edit' pk=p.pk %}">
 | 
			
		||||
          <span title="{% translate 'Edit' %} {{ page }}">{% translate 'Edit' %} </span>
 | 
			
		||||
      </a>
 | 
			
		||||
  </li>
 | 
			
		||||
  {% endfor %}
 | 
			
		||||
  </ul>
 | 
			
		||||
  {% else %}
 | 
			
		||||
  {% trans 'You are not listed as a program editor yet' %}
 | 
			
		||||
  {% endif %}
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
{% load static i18n thumbnail aircox %}<!doctype html>
 | 
			
		||||
{% comment %}
 | 
			
		||||
Base website template. It displays various elements depending on context
 | 
			
		||||
variables.
 | 
			
		||||
@ -10,8 +11,6 @@ Usefull context:
 | 
			
		||||
- sidebar_url_name: url name sidebar item complete list
 | 
			
		||||
- sidebar_url_parent: parent page for sidebar items complete list
 | 
			
		||||
{% endcomment %}
 | 
			
		||||
{% load static i18n thumbnail aircox %}
 | 
			
		||||
 | 
			
		||||
<html>
 | 
			
		||||
    <head>
 | 
			
		||||
        <meta charset="utf-8" />
 | 
			
		||||
@ -71,12 +70,21 @@ Usefull context:
 | 
			
		||||
                            {% translate "Admin" %}
 | 
			
		||||
                        </a>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                        {% if user.is_authenticated %}
 | 
			
		||||
                        <a class="nav-item" href="{% url "profile" %}" target="new">
 | 
			
		||||
                            {% translate "Profile" %}
 | 
			
		||||
                        </a>
 | 
			
		||||
                        <a class="nav-item" href="{% url 'logout' %}">
 | 
			
		||||
                            <i title="{% translate 'disconnect' %}" class="fa fa-power-off"></i>
 | 
			
		||||
                        </a>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                        {% endblock %}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% endblock %}
 | 
			
		||||
                </nav>
 | 
			
		||||
                {% endblock %}
 | 
			
		||||
 | 
			
		||||
                {% block secondary-nav %}{% endblock %}
 | 
			
		||||
                {% endblock %}
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            {% block main-container %}
 | 
			
		||||
@ -90,6 +98,8 @@ Usefull context:
 | 
			
		||||
                {% endblock %}
 | 
			
		||||
                {% endspaceless %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                {% block header-container %}
 | 
			
		||||
                {% if page or cover or title %}
 | 
			
		||||
                <header class="container header preview preview-header {% if cover %}has-cover{% endif %}">
 | 
			
		||||
@ -101,6 +111,7 @@ Usefull context:
 | 
			
		||||
                    {% block headings %}
 | 
			
		||||
                    <div>
 | 
			
		||||
                        <h1 class="heading title is-1 {% block title-class %}{% endblock %}">{% block title %}{{ title|default:"" }}{% endblock %}</h1>
 | 
			
		||||
                        {% include "aircox/edit-link.html" %}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div>
 | 
			
		||||
                        {% spaceless %}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										20
									
								
								aircox/templates/aircox/edit-link.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								aircox/templates/aircox/edit-link.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
			
		||||
{% load aircox i18n %}
 | 
			
		||||
{% block user-actions-container %}
 | 
			
		||||
{% has_perm page page.program.change_permission_codename simple=True as can_edit %}
 | 
			
		||||
{% if user.is_authenticated and can_edit %}
 | 
			
		||||
{% with request.resolver_match.view_name as view_name %}
 | 
			
		||||
   
 | 
			
		||||
  {% if view_name in 'program-edit,bla' %}
 | 
			
		||||
  <!--
 | 
			
		||||
  <a href="{% url 'program-detail' page.slug %}" target="_self">
 | 
			
		||||
      <span title="{% translate 'View' %} {{ page }}">{% translate 'View' %} 👁 </span>
 | 
			
		||||
  </a>
 | 
			
		||||
  -->
 | 
			
		||||
  {% else %}
 | 
			
		||||
  <a href="{% url view_name|edit_view page.pk %}" target="_self">
 | 
			
		||||
      <span title="{% translate 'Edit' %} {{ page }}">{% translate 'Edit' %} 🖉  </span>
 | 
			
		||||
  </a>
 | 
			
		||||
  {% endif %}
 | 
			
		||||
{% endwith %}
 | 
			
		||||
{% endif %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
							
								
								
									
										30
									
								
								aircox/templates/aircox/episode_form.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								aircox/templates/aircox/episode_form.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
			
		||||
{% extends "aircox/basepage_detail.html" %}
 | 
			
		||||
{% load static i18n humanize honeypot aircox %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{% block head_extra %}
 | 
			
		||||
  {{ form.media }}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block init-scripts %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block comments %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content-container %}
 | 
			
		||||
<section class="container">
 | 
			
		||||
<form method="post" enctype="multipart/form-data">{% csrf_token %}
 | 
			
		||||
    <table>
 | 
			
		||||
        {{ form.as_table }}
 | 
			
		||||
        {% render_honeypot_field "website" %}
 | 
			
		||||
    </table>
 | 
			
		||||
    <br/>
 | 
			
		||||
    {{ forms }}
 | 
			
		||||
    <br/>
 | 
			
		||||
    <input type="submit" value="Update" class="button is-success">
 | 
			
		||||
</form>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
</section>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@ -18,7 +18,7 @@
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
{% block content-container %}
 | 
			
		||||
<article class="message is-danger">
 | 
			
		||||
    <div class="message-header">
 | 
			
		||||
        <p>{% block error_title %}{% trans "An error occurred" %}{% endblock %}</p>
 | 
			
		||||
 | 
			
		||||
@ -10,21 +10,6 @@ Context:
 | 
			
		||||
- related_url: url to the full list of related_objects
 | 
			
		||||
{% endcomment %}
 | 
			
		||||
 | 
			
		||||
{% block top-nav-tools %}
 | 
			
		||||
{% has_perm page "change" as can_edit %}
 | 
			
		||||
{% if can_edit %}
 | 
			
		||||
<a class="navbar-item" href="{{ page|admin_url:'change' }}"
 | 
			
		||||
    target="new">
 | 
			
		||||
    <span class="icon is-small">
 | 
			
		||||
        <i class="fa fa-pen"></i>
 | 
			
		||||
    </span> 
 | 
			
		||||
    <span>{% translate "Edit" %}</span>
 | 
			
		||||
</a>
 | 
			
		||||
{% endif %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{% block breadcrumbs %}
 | 
			
		||||
{% if parent %}
 | 
			
		||||
    {% include "./widgets/breadcrumbs.html" with page=parent %}
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,18 @@
 | 
			
		||||
{% comment %}Detail page of a show{% endcomment %}
 | 
			
		||||
{% load i18n aircox %}
 | 
			
		||||
 | 
			
		||||
{% block top-nav-tools %}
 | 
			
		||||
{% has_perm page page.change_permission_codename simple=True as can_edit %}
 | 
			
		||||
{% if can_edit %}
 | 
			
		||||
<a class="navbar-item" href="{% url 'program-edit' page.pk %}" target="_self">
 | 
			
		||||
    <span class="icon is-small">
 | 
			
		||||
        <i class="fa fa-pen"></i>
 | 
			
		||||
    </span> 
 | 
			
		||||
    <span>{% translate "Edit" %}</span>
 | 
			
		||||
</a>
 | 
			
		||||
{% endif %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content-container %}
 | 
			
		||||
{% with schedules=program.schedule_set.all %}
 | 
			
		||||
{% if schedules %}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										28
									
								
								aircox/templates/aircox/program_form.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								aircox/templates/aircox/program_form.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
			
		||||
{% extends "aircox/page_detail.html" %}
 | 
			
		||||
{% load static i18n humanize honeypot aircox %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{% block head_extra %}
 | 
			
		||||
  {{ form.media }}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block init-scripts %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block comments %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content-container %}
 | 
			
		||||
<section class="container">
 | 
			
		||||
<div>
 | 
			
		||||
<form method="post" enctype="multipart/form-data">{% csrf_token %}
 | 
			
		||||
    <table>
 | 
			
		||||
        {{ form.as_table }}
 | 
			
		||||
        {% render_honeypot_field "website" %}
 | 
			
		||||
    </table>
 | 
			
		||||
    <br/>
 | 
			
		||||
    <input type="submit" value="Update" class="button is-success">
 | 
			
		||||
</form>
 | 
			
		||||
</div>
 | 
			
		||||
</section>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@ -25,12 +25,31 @@
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
{% if not object.content %}
 | 
			
		||||
    {% with object.parent.content as content %}
 | 
			
		||||
    {{ block.super }}
 | 
			
		||||
    {% endwith %}
 | 
			
		||||
{% else %}
 | 
			
		||||
    {{ block.super }}
 | 
			
		||||
{% block actions %}
 | 
			
		||||
{% if object.sound_set.public.count %}
 | 
			
		||||
<button class="button" @click="player.playButtonClick($event)"
 | 
			
		||||
        data-sounds="{{ object.podcasts|json }}">
 | 
			
		||||
    <span class="icon is-small">
 | 
			
		||||
        <span class="fas fa-play"></span>
 | 
			
		||||
    </span>
 | 
			
		||||
</button>
 | 
			
		||||
{% endif %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block actions %}
 | 
			
		||||
{% has_perm page object.program.change_permission_codename simple=True as can_edit %}
 | 
			
		||||
{% if can_edit %}
 | 
			
		||||
  <a class="button" href="{% url 'episode-edit' object.pk %}" target="_self">
 | 
			
		||||
    <span class="icon is-small"><i class="fas fa-pen" alt="{% trans 'edit' %}"></i></span>
 | 
			
		||||
  </a>
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
{% if object.sound_set.public.count %}
 | 
			
		||||
<button class="button" @click="player.playButtonClick($event)"
 | 
			
		||||
        data-sounds="{{ object.podcasts|json }}">
 | 
			
		||||
    <span class="icon is-small">
 | 
			
		||||
        <span class="fas fa-play"></span>
 | 
			
		||||
    </span>
 | 
			
		||||
</button>
 | 
			
		||||
{% endif %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										18
									
								
								aircox/templates/registration/login.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								aircox/templates/registration/login.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
			
		||||
{% extends "aircox/base.html" %}
 | 
			
		||||
{% load i18n aircox %}
 | 
			
		||||
 | 
			
		||||
{% block content-container %}
 | 
			
		||||
<div class="container content page-content">
 | 
			
		||||
<h2>{% trans "Log in" %}</h2>
 | 
			
		||||
<br/>
 | 
			
		||||
<form method="post" action="{% url 'login' %}">
 | 
			
		||||
  {% csrf_token %}
 | 
			
		||||
  <table>
 | 
			
		||||
  {{ form.as_table }}
 | 
			
		||||
  </table>
 | 
			
		||||
  <br/>
 | 
			
		||||
  <button class="button" type="submit">{% trans "Log in" %}</button>
 | 
			
		||||
  <input type="hidden" name="next" value="{{ next }}">
 | 
			
		||||
</form>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@ -57,11 +57,16 @@ def do_get_tracks(obj):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.simple_tag(name="has_perm", takes_context=True)
 | 
			
		||||
def do_has_perm(context, obj, perm, user=None):
 | 
			
		||||
def do_has_perm(context, obj, perm, user=None, simple=False):
 | 
			
		||||
    """Return True if ``user.has_perm('[APP].[perm]_[MODEL]')``"""
 | 
			
		||||
    if not obj:
 | 
			
		||||
        return
 | 
			
		||||
    if user is None:
 | 
			
		||||
        user = context["request"].user
 | 
			
		||||
    return user.has_perm("{}.{}_{}".format(obj._meta.app_label, perm, obj._meta.model_name))
 | 
			
		||||
    if simple:
 | 
			
		||||
        return user.has_perm("aircox.{}".format(perm)) or user.is_superuser
 | 
			
		||||
    else:
 | 
			
		||||
        return user.has_perm("{}.{}_{}".format(obj._meta.app_label, perm, obj._meta.model_name))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter(name="is_diffusion")
 | 
			
		||||
@ -93,6 +98,8 @@ def do_player_live_attr(context):
 | 
			
		||||
@register.simple_tag(name="nav_items", takes_context=True)
 | 
			
		||||
def do_nav_items(context, menu, **kwargs):
 | 
			
		||||
    """Render navigation items for the provided menu name."""
 | 
			
		||||
    if not getattr(context["request"], "station"):
 | 
			
		||||
        return []
 | 
			
		||||
    station, request = context["station"], context["request"]
 | 
			
		||||
    return [(item, item.render(request, **kwargs)) for item in station.navitem_set.filter(menu=menu)]
 | 
			
		||||
 | 
			
		||||
@ -125,3 +132,8 @@ def do_verbose_name(obj, plural=False):
 | 
			
		||||
    if isinstance(obj, str):
 | 
			
		||||
        return obj
 | 
			
		||||
    return obj._meta.verbose_name_plural if plural else obj._meta.verbose_name
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter(name="edit_view")
 | 
			
		||||
def do_edit_view(obj):
 | 
			
		||||
    return "%s-edit" % obj.split("-")[0]
 | 
			
		||||
 | 
			
		||||
@ -157,3 +157,8 @@ def tracks(episode, sound):
 | 
			
		||||
    items += [baker.prepare(models.Track, sound=sound, position=i, timestamp=i * 60) for i in range(0, 3)]
 | 
			
		||||
    models.Track.objects.bulk_create(items)
 | 
			
		||||
    return items
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def user():
 | 
			
		||||
    return User.objects.create_user(username="user1", password="bar")
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										36
									
								
								aircox/tests/test_permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								aircox/tests/test_permissions.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,36 @@
 | 
			
		||||
import pytest
 | 
			
		||||
from django.contrib.auth.models import User, Group
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.django_db()
 | 
			
		||||
def test_no_admin(user, client):
 | 
			
		||||
    client.force_login(user)
 | 
			
		||||
    response = client.get("/admin/")
 | 
			
		||||
    assert response.status_code != 200
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.django_db()
 | 
			
		||||
def test_user_cannot_change_program_or_episode(user, client, program):
 | 
			
		||||
    assert not user.has_perm("aircox.change_program")
 | 
			
		||||
    assert not user.has_perm("aircox.change_episode")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.django_db()
 | 
			
		||||
def test_group_can_change_program(user, client, program):
 | 
			
		||||
    assert program.editors in Group.objects.all()
 | 
			
		||||
    assert not user.has_perm("aircox.%s" % program.change_permission_codename)
 | 
			
		||||
    user.groups.add(program.editors)
 | 
			
		||||
    user = User.objects.get(pk=user.pk)  # reload user in order to have permissions set
 | 
			
		||||
    assert program.editors in user.groups.all()
 | 
			
		||||
    assert user.has_perm("aircox.%s" % program.change_permission_codename)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.django_db()
 | 
			
		||||
def test_group_change_program(user, client, program):
 | 
			
		||||
    client.force_login(user)
 | 
			
		||||
    response = client.get(reverse("program-edit", kwargs={"pk": program.pk}))
 | 
			
		||||
    assert response.status_code == 403
 | 
			
		||||
    user.groups.add(program.editors)
 | 
			
		||||
    response = client.get(reverse("program-edit", kwargs={"pk": program.pk}))
 | 
			
		||||
    assert response.status_code == 200
 | 
			
		||||
							
								
								
									
										22
									
								
								aircox/tests/test_profile.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								aircox/tests/test_profile.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
			
		||||
import pytest
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.django_db()
 | 
			
		||||
def test_authenticate(user, client, program):
 | 
			
		||||
    r = client.get(reverse("login"))
 | 
			
		||||
    assert r.status_code == 200
 | 
			
		||||
    assert b"id_username" in r.content
 | 
			
		||||
    r = client.post(reverse("login"), kwargs={"username": "foo", "password": "bar"})
 | 
			
		||||
    assert b"errorlist" in r.content
 | 
			
		||||
    assert client.login(username="user1", password="bar")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.django_db()
 | 
			
		||||
def test_profile_programs(user, client, program):
 | 
			
		||||
    client.force_login(user)
 | 
			
		||||
    r = client.get(reverse("profile"))
 | 
			
		||||
    assert program.title not in r.content.decode("utf-8")
 | 
			
		||||
    user.groups.add(program.editors)
 | 
			
		||||
    r = client.get(reverse("profile"))
 | 
			
		||||
    assert program.title in r.content.decode("utf-8")
 | 
			
		||||
							
								
								
									
										40
									
								
								aircox/tests/test_program.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								aircox/tests/test_program.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,40 @@
 | 
			
		||||
import pytest
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
from django.core.files.uploadedfile import SimpleUploadedFile
 | 
			
		||||
 | 
			
		||||
from aircox.models import Program
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
png_content = (
 | 
			
		||||
    b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS\xde"
 | 
			
		||||
    + b"\x00\x00\x00\x0cIDATx\x9cc`\xf8\xcf\x00\x00\x02\x02\x01\x00{\t\x81x\x00\x00\x00\x00IEND\xaeB`\x82"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.django_db()
 | 
			
		||||
def test_edit_program(user, client, program):
 | 
			
		||||
    client.force_login(user)
 | 
			
		||||
    response = client.get(reverse("program-detail", kwargs={"slug": program.slug}))
 | 
			
		||||
    assert response.status_code == 200
 | 
			
		||||
    assert "🖉 ".encode() not in response.content
 | 
			
		||||
    user.groups.add(program.editors)
 | 
			
		||||
    response = client.get(reverse("program-detail", kwargs={"slug": program.slug}))
 | 
			
		||||
    assert "🖉 ".encode() in response.content
 | 
			
		||||
    assert b"foobar" not in response.content
 | 
			
		||||
    response = client.post(reverse("program-edit", kwargs={"pk": program.pk}), {"content": "foobar"})
 | 
			
		||||
    response = client.get(reverse("program-detail", kwargs={"slug": program.slug}))
 | 
			
		||||
    assert b"foobar" in response.content
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.django_db()
 | 
			
		||||
def test_add_cover(user, client, program):
 | 
			
		||||
    assert program.cover is None
 | 
			
		||||
    user.groups.add(program.editors)
 | 
			
		||||
    client.force_login(user)
 | 
			
		||||
    cover = SimpleUploadedFile("cover1.png", png_content, content_type="image/png")
 | 
			
		||||
    r = client.post(
 | 
			
		||||
        reverse("program-edit", kwargs={"pk": program.pk}), {"content": "foobar", "new_cover": cover}, follow=True
 | 
			
		||||
    )
 | 
			
		||||
    assert r.status_code == 200
 | 
			
		||||
    p = Program.objects.get(pk=program.pk)
 | 
			
		||||
    assert "cover1.png" in p.cover.url
 | 
			
		||||
@ -61,6 +61,11 @@ urls = [
 | 
			
		||||
        views.EpisodeDetailView.as_view(),
 | 
			
		||||
        name="episode-detail",
 | 
			
		||||
    ),
 | 
			
		||||
   path(
 | 
			
		||||
        _("episode/<pk>/edit/"),
 | 
			
		||||
        views.EpisodeUpdateView.as_view(),
 | 
			
		||||
        name="episode-edit",
 | 
			
		||||
    ),
 | 
			
		||||
    path(_("podcasts/"), views.PodcastListView.as_view(), name="podcast-list"),
 | 
			
		||||
    path(_("podcasts/c/<slug:category_slug>/"), views.PodcastListView.as_view(), name="podcast-list"),
 | 
			
		||||
    # ---- timetable
 | 
			
		||||
@ -109,6 +114,11 @@ urls = [
 | 
			
		||||
    path(_("programs/<slug:parent_slug>/podcasts/"), views.PodcastListView.as_view(), name="podcast-list"),
 | 
			
		||||
    path(_("programs/<slug:parent_slug>/episodes/"), views.EpisodeListView.as_view(), name="episode-list"),
 | 
			
		||||
    path(_("programs/<slug:parent_slug>/diffusions/"), views.DiffusionListView.as_view(), name="diffusion-list"),
 | 
			
		||||
    path(
 | 
			
		||||
        _("program/<pk>/edit/"),
 | 
			
		||||
        views.ProgramUpdateView.as_view(),
 | 
			
		||||
        name="program-edit",
 | 
			
		||||
    ),
 | 
			
		||||
    path(
 | 
			
		||||
        "errors/no-station",
 | 
			
		||||
        views.errors.NoStationErrorView.as_view(),
 | 
			
		||||
@ -119,4 +129,6 @@ urls = [
 | 
			
		||||
        views.PageListView.as_view(model=models.Page, attach_to_value=models.StaticPage.Target.PAGES),
 | 
			
		||||
        name="page-list",
 | 
			
		||||
    ),
 | 
			
		||||
    path("gestion/", views.profile, name="profile"),
 | 
			
		||||
    path("accounts/profile/", views.profile, name="profile"),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ from . import admin, errors
 | 
			
		||||
from .article import ArticleDetailView, ArticleListView
 | 
			
		||||
from .base import BaseAPIView, BaseView
 | 
			
		||||
from .diffusion import DiffusionListView, TimeTableView
 | 
			
		||||
from .episode import EpisodeDetailView, EpisodeListView, PodcastListView
 | 
			
		||||
from .episode import EpisodeDetailView, EpisodeListView, PodcastListView, EpisodeUpdateView
 | 
			
		||||
from .home import HomeView
 | 
			
		||||
from .log import LogListAPIView, LogListView
 | 
			
		||||
from .page import (
 | 
			
		||||
@ -11,11 +11,13 @@ from .page import (
 | 
			
		||||
    PageDetailView,
 | 
			
		||||
    PageListView,
 | 
			
		||||
)
 | 
			
		||||
from .profile import profile
 | 
			
		||||
from .program import (
 | 
			
		||||
    ProgramDetailView,
 | 
			
		||||
    ProgramListView,
 | 
			
		||||
    ProgramPageDetailView,
 | 
			
		||||
    ProgramPageListView,
 | 
			
		||||
    ProgramUpdateView,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
__all__ = (
 | 
			
		||||
@ -30,6 +32,7 @@ __all__ = (
 | 
			
		||||
    "EpisodeDetailView",
 | 
			
		||||
    "EpisodeListView",
 | 
			
		||||
    "PodcastListView",
 | 
			
		||||
    "EpisodeUpdateView",
 | 
			
		||||
    "HomeView",
 | 
			
		||||
    "LogListAPIView",
 | 
			
		||||
    "LogListView",
 | 
			
		||||
@ -37,10 +40,12 @@ __all__ = (
 | 
			
		||||
    "BasePageListView",
 | 
			
		||||
    "PageDetailView",
 | 
			
		||||
    "PageListView",
 | 
			
		||||
    "profile",
 | 
			
		||||
    "ProgramDetailView",
 | 
			
		||||
    "ProgramListView",
 | 
			
		||||
    "ProgramPageDetailView",
 | 
			
		||||
    "ProgramPageListView",
 | 
			
		||||
    "ProgramUpdateView",
 | 
			
		||||
    "attached",
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -50,13 +50,9 @@ class BaseView(TemplateResponseMixin, ContextMixin):
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        kwargs.setdefault("station", self.station)
 | 
			
		||||
        kwargs.setdefault("page", self.get_page())
 | 
			
		||||
        kwargs.setdefault("header_template_name", self.header_template_name)
 | 
			
		||||
 | 
			
		||||
        if "audio_streams" not in kwargs:
 | 
			
		||||
            kwargs["audio_streams"] = self.station.streams
 | 
			
		||||
 | 
			
		||||
        if "model" not in kwargs:
 | 
			
		||||
            model = getattr(self, "model", None) or hasattr(self, "object") and type(self.object)
 | 
			
		||||
            kwargs["model"] = model
 | 
			
		||||
 | 
			
		||||
@ -1,14 +1,26 @@
 | 
			
		||||
from django.shortcuts import reverse
 | 
			
		||||
from django.contrib.auth.mixins import UserPassesTestMixin
 | 
			
		||||
from django.forms import ModelForm, FileField
 | 
			
		||||
from django.forms.models import modelformset_factory
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
 | 
			
		||||
from ckeditor.fields import RichTextField
 | 
			
		||||
from filer.models.filemodels import File
 | 
			
		||||
 | 
			
		||||
from aircox.controllers.sound_file import SoundFile
 | 
			
		||||
from aircox.models import Track
 | 
			
		||||
 | 
			
		||||
from ..filters import EpisodeFilters
 | 
			
		||||
from ..models import Episode, Program, StaticPage
 | 
			
		||||
from .page import PageListView
 | 
			
		||||
from .program import ProgramPageDetailView
 | 
			
		||||
from .program import ProgramPageDetailView, BaseProgramMixin
 | 
			
		||||
from .page import PageUpdateView
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = (
 | 
			
		||||
    "EpisodeDetailView",
 | 
			
		||||
    "EpisodeListView",
 | 
			
		||||
    "PodcastListView",
 | 
			
		||||
    "EpisodeUpdateView",
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -39,3 +51,45 @@ class EpisodeListView(PageListView):
 | 
			
		||||
class PodcastListView(EpisodeListView):
 | 
			
		||||
    attach_to_value = StaticPage.Target.PODCASTS
 | 
			
		||||
    queryset = Episode.objects.published().with_podcasts().order_by("-pub_date")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EpisodeForm(ModelForm):
 | 
			
		||||
    content = RichTextField()
 | 
			
		||||
    new_podcast = FileField(required=False)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Episode
 | 
			
		||||
        fields = ["content"]
 | 
			
		||||
 | 
			
		||||
    def save(self, commit=True):
 | 
			
		||||
        file_obj = self.cleaned_data["new_podcast"]
 | 
			
		||||
        if file_obj:
 | 
			
		||||
            obj, _ = File.objects.get_or_create(original_filename=file_obj.name, file=file_obj)
 | 
			
		||||
            sound_file = SoundFile(obj.path)
 | 
			
		||||
            sound_file.sync(
 | 
			
		||||
                program=self.instance.program, episode=self.instance, type=0, is_public=True, is_downloadable=True
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EpisodeUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
 | 
			
		||||
    model = Episode
 | 
			
		||||
    form_class = EpisodeForm
 | 
			
		||||
 | 
			
		||||
    def get_sidebar_queryset(self):
 | 
			
		||||
        return super().get_sidebar_queryset().filter(parent=self.program)
 | 
			
		||||
 | 
			
		||||
    def test_func(self):
 | 
			
		||||
        program = self.get_object().program
 | 
			
		||||
        return self.request.user.has_perm("aircox.%s" % program.change_permission_codename)
 | 
			
		||||
 | 
			
		||||
    def get_success_url(self):
 | 
			
		||||
        return reverse("episode-detail", kwargs={"slug": self.get_object().slug})
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        context = super().get_context_data(**kwargs)
 | 
			
		||||
        formset = modelformset_factory(Track, fields=["title", "artist"])
 | 
			
		||||
        context["forms"] = formset(queryset=Track.objects.filter(episode=self.object))
 | 
			
		||||
        return context
 | 
			
		||||
 | 
			
		||||
    # def post(self, request, *args, **kwargs):
 | 
			
		||||
    # def form_valid(self, formset,day_form):
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
from django.http import Http404, HttpResponse
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from django.views.generic import DetailView, ListView
 | 
			
		||||
from django.views.generic.edit import UpdateView
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
from honeypot.decorators import check_honeypot
 | 
			
		||||
 | 
			
		||||
@ -196,3 +197,10 @@ class PageDetailView(BasePageDetailView):
 | 
			
		||||
        comment.page = self.object
 | 
			
		||||
        comment.save()
 | 
			
		||||
        return self.get(request, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PageUpdateView(BaseView, UpdateView):
 | 
			
		||||
    context_object_name = "page"
 | 
			
		||||
 | 
			
		||||
    def get_page(self):
 | 
			
		||||
        return self.object
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										15
									
								
								aircox/views/profile.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								aircox/views/profile.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
			
		||||
from django.contrib.auth.decorators import login_required
 | 
			
		||||
from django.template.response import TemplateResponse
 | 
			
		||||
 | 
			
		||||
from aircox.models import Program
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@login_required
 | 
			
		||||
def profile(request):
 | 
			
		||||
    programs = []
 | 
			
		||||
    ugroups = request.user.groups.all()
 | 
			
		||||
    for p in Program.objects.all():
 | 
			
		||||
        if p.editors in ugroups:
 | 
			
		||||
            programs.append(p)
 | 
			
		||||
    context = {"user": request.user, "programs": programs}
 | 
			
		||||
    return TemplateResponse(request, "accounts/profile.html", context)
 | 
			
		||||
@ -1,10 +1,16 @@
 | 
			
		||||
import random
 | 
			
		||||
 | 
			
		||||
from django.contrib.auth.mixins import UserPassesTestMixin
 | 
			
		||||
from django.forms import ModelForm, ImageField
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
 | 
			
		||||
from ..models import Article, Page, Program, StaticPage, Episode
 | 
			
		||||
from ckeditor.fields import RichTextField
 | 
			
		||||
from filer.models.imagemodels import Image
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
from ..models import Article, Episode, Page, Program, StaticPage
 | 
			
		||||
from .mixins import ParentMixin
 | 
			
		||||
from .page import PageDetailView, PageListView
 | 
			
		||||
from .page import PageDetailView, PageListView, PageUpdateView
 | 
			
		||||
 | 
			
		||||
__all__ = ["ProgramPageDetailView", "ProgramDetailView", "ProgramPageListView"]
 | 
			
		||||
 | 
			
		||||
@ -49,6 +55,40 @@ class ProgramDetailView(BaseProgramMixin, PageDetailView):
 | 
			
		||||
            **kwargs,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def get_template_names(self):
 | 
			
		||||
        return super().get_template_names() + ["aircox/program_detail.html"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ProgramForm(ModelForm):
 | 
			
		||||
    content = RichTextField()
 | 
			
		||||
    new_cover = ImageField(required=False)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Program
 | 
			
		||||
        fields = ["content"]
 | 
			
		||||
 | 
			
		||||
    def save(self, commit=True):
 | 
			
		||||
        file_obj = self.cleaned_data["new_cover"]
 | 
			
		||||
        if file_obj:
 | 
			
		||||
            obj, _ = Image.objects.get_or_create(original_filename=file_obj.name, file=file_obj)
 | 
			
		||||
            self.instance.cover = obj
 | 
			
		||||
        super().save(commit=commit)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ProgramUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
 | 
			
		||||
    model = Program
 | 
			
		||||
    form_class = ProgramForm
 | 
			
		||||
 | 
			
		||||
    def get_sidebar_queryset(self):
 | 
			
		||||
        return super().get_sidebar_queryset().filter(parent=self.program)
 | 
			
		||||
 | 
			
		||||
    def test_func(self):
 | 
			
		||||
        program = self.get_object()
 | 
			
		||||
        return self.request.user.has_perm("aircox.%s" % program.change_permission_codename)
 | 
			
		||||
 | 
			
		||||
    def get_success_url(self):
 | 
			
		||||
        return reverse("program-detail", kwargs={"slug": self.get_object().slug})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ProgramListView(PageListView):
 | 
			
		||||
    model = Program
 | 
			
		||||
 | 
			
		||||
@ -238,6 +238,7 @@ TEMPLATES = [
 | 
			
		||||
                "django.template.context_processors.static",
 | 
			
		||||
                "django.template.context_processors.tz",
 | 
			
		||||
                "django.contrib.messages.context_processors.messages",
 | 
			
		||||
                "aircox.context_processors.station",
 | 
			
		||||
            ),
 | 
			
		||||
            "loaders": (
 | 
			
		||||
                "django.template.loaders.filesystem.Loader",
 | 
			
		||||
@ -249,3 +250,5 @@ TEMPLATES = [
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
WSGI_APPLICATION = "instance.wsgi.application"
 | 
			
		||||
 | 
			
		||||
LOGOUT_REDIRECT_URL = "/"
 | 
			
		||||
 | 
			
		||||
@ -23,6 +23,7 @@ import aircox.urls
 | 
			
		||||
 | 
			
		||||
urlpatterns = aircox.urls.urls + [
 | 
			
		||||
    path("admin/", admin.site.urls),
 | 
			
		||||
    path("accounts/", include("django.contrib.auth.urls")),
 | 
			
		||||
    path("ckeditor/", include("ckeditor_uploader.urls")),
 | 
			
		||||
    path("filer/", include("filer.urls")),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user