integrate statistics
This commit is contained in:
		@ -4,6 +4,14 @@ from django.db import migrations, models
 | 
				
			|||||||
import django.db.models.deletion
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def init_groups_and_permissions(app, schema_editor):
 | 
				
			||||||
 | 
					    from aircox.permissions import program_permissions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Program = app.get_model("aircox", "Program")
 | 
				
			||||||
 | 
					    for program in Program.objects.all():
 | 
				
			||||||
 | 
					        program_permissions.init(program)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
    dependencies = [
 | 
					    dependencies = [
 | 
				
			||||||
        ("auth", "0012_alter_user_first_name_max_length"),
 | 
					        ("auth", "0012_alter_user_first_name_max_length"),
 | 
				
			||||||
@ -22,4 +30,5 @@ class Migration(migrations.Migration):
 | 
				
			|||||||
                verbose_name="editors",
 | 
					                verbose_name="editors",
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RunPython(init_groups_and_permissions),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
				
			|||||||
@ -336,7 +336,7 @@ class Comment(Renderable, models.Model):
 | 
				
			|||||||
        return Page.objects.select_subclasses().filter(id=self.page_id).first()
 | 
					        return Page.objects.select_subclasses().filter(id=self.page_id).first()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_absolute_url(self):
 | 
					    def get_absolute_url(self):
 | 
				
			||||||
        return self.parent.get_absolute_url() + f"#comment-{self.pk}"
 | 
					        return self.parent.get_absolute_url() + f"#{self._meta.label_lower}-{self.pk}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        verbose_name = _("Comment")
 | 
					        verbose_name = _("Comment")
 | 
				
			||||||
 | 
				
			|||||||
@ -1,8 +1,7 @@
 | 
				
			|||||||
import os
 | 
					import os
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.conf import settings as conf
 | 
					from django.conf import settings as conf
 | 
				
			||||||
from django.contrib.auth.models import Group, Permission
 | 
					from django.contrib.auth.models import Group
 | 
				
			||||||
from django.contrib.contenttypes.models import ContentType
 | 
					 | 
				
			||||||
from django.db import models
 | 
					from django.db import models
 | 
				
			||||||
from django.utils.translation import gettext_lazy as _
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -117,37 +116,6 @@ class Program(Page):
 | 
				
			|||||||
        os.makedirs(path, exist_ok=True)
 | 
					        os.makedirs(path, exist_ok=True)
 | 
				
			||||||
        return os.path.exists(path)
 | 
					        return os.path.exists(path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def can_update(self, user):
 | 
					 | 
				
			||||||
        """Return True if user can update program."""
 | 
					 | 
				
			||||||
        if user.is_superuser:
 | 
					 | 
				
			||||||
            return True
 | 
					 | 
				
			||||||
        perm = self._perm_update_codename.format(self=self)
 | 
					 | 
				
			||||||
        return user.has_perm("aircox." + perm)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # permissions and editor group format. Use of pk in codename makes it
 | 
					 | 
				
			||||||
    # consistent in case program title changes.
 | 
					 | 
				
			||||||
    _editor_group_name = "{self.title}: editors"
 | 
					 | 
				
			||||||
    _perm_update_codename = "program_{self.pk}_update"
 | 
					 | 
				
			||||||
    _perm_update_name = "{self.title}: update"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def init_editor_group(self):
 | 
					 | 
				
			||||||
        if not self.editors_group:
 | 
					 | 
				
			||||||
            name = self._editor_group_name.format(self=self)
 | 
					 | 
				
			||||||
            self.editors_group, created = Group.objects.get_or_create(name=name)
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            created = False
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if created:
 | 
					 | 
				
			||||||
            if not self.pk:
 | 
					 | 
				
			||||||
                self.save(check_groups=False)
 | 
					 | 
				
			||||||
            permission, _ = Permission.objects.get_or_create(
 | 
					 | 
				
			||||||
                codename=self._perm_update_codename.format(self=self),
 | 
					 | 
				
			||||||
                content_type=ContentType.objects.get_for_model(self),
 | 
					 | 
				
			||||||
                defaults={"name": self._perm_update_name.format(self=self)},
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            if permission not in self.editors_group.permissions.all():
 | 
					 | 
				
			||||||
                self.editors_group.permissions.add(permission)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        verbose_name = _("Program")
 | 
					        verbose_name = _("Program")
 | 
				
			||||||
        verbose_name_plural = _("Programs")
 | 
					        verbose_name_plural = _("Programs")
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										82
									
								
								aircox/permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								aircox/permissions.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,82 @@
 | 
				
			|||||||
 | 
					# Provide permissions handling
 | 
				
			||||||
 | 
					# we don't import models at module level in order to avoid migration problems
 | 
				
			||||||
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
 | 
					from django.contrib.auth.models import Group, Permission
 | 
				
			||||||
 | 
					from django.contrib.contenttypes.models import ContentType
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .models import Program
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					__all__ = ("PagePermissions", "program_permissions")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PagePermissions:
 | 
				
			||||||
 | 
					    """Handles obj permissions initialization of page subclass."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    model = None
 | 
				
			||||||
 | 
					    groups = ({"label": _("editors"), "field": "editors_group_id", "perms": ["update"]},)
 | 
				
			||||||
 | 
					    """Groups informations initialized."""
 | 
				
			||||||
 | 
					    groups_name_format = "{obj.title}: {group_label}"
 | 
				
			||||||
 | 
					    """Format used for groups name."""
 | 
				
			||||||
 | 
					    perms_name_format = "{obj.title}: can {perm}"
 | 
				
			||||||
 | 
					    """Format used for permission name (displayed to humans)."""
 | 
				
			||||||
 | 
					    perms_codename_format = "{obj._meta.label_lower}_{obj.pk}_{perm}"
 | 
				
			||||||
 | 
					    """Format used for permissions codename."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, model):
 | 
				
			||||||
 | 
					        self.model = model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def can(self, user, perm, obj):
 | 
				
			||||||
 | 
					        """Return True wether if user can edit Program or its children."""
 | 
				
			||||||
 | 
					        from .models.page import ChildPage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        breakpoint()
 | 
				
			||||||
 | 
					        if isinstance(obj, ChildPage):
 | 
				
			||||||
 | 
					            obj = obj.parent_subclass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if not isinstance(obj, self.model):
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if user.is_superuser:
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        perm = self.perms_codename_format.format(self=self, perm=perm)
 | 
				
			||||||
 | 
					        return user.has_perm(perm)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # TODO: bulk init
 | 
				
			||||||
 | 
					    def init(self, obj):
 | 
				
			||||||
 | 
					        """Initialize permissions for the provided obj."""
 | 
				
			||||||
 | 
					        created_groups = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # init groups
 | 
				
			||||||
 | 
					        for infos in self.groups:
 | 
				
			||||||
 | 
					            group = getattr(obj, infos["field"])
 | 
				
			||||||
 | 
					            if not group:
 | 
				
			||||||
 | 
					                group, created = self.init_group(obj, infos)
 | 
				
			||||||
 | 
					                setattr(obj, infos["field"], group.pk)
 | 
				
			||||||
 | 
					                created and created_groups.append((group, infos))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if created_groups:
 | 
				
			||||||
 | 
					            obj.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # init perms
 | 
				
			||||||
 | 
					        for group, infos in created_groups:
 | 
				
			||||||
 | 
					            self.init_perms(obj, group, infos)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def init_group(self, obj, infos):
 | 
				
			||||||
 | 
					        name = self.groups_name_format.format(obj=obj, group_label=infos["label"])
 | 
				
			||||||
 | 
					        return Group.objects.get_or_create(name=name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def init_perms(self, obj, group, infos):
 | 
				
			||||||
 | 
					        # TODO: avoid multiple database hits
 | 
				
			||||||
 | 
					        for name in infos["perms"]:
 | 
				
			||||||
 | 
					            perm, _ = Permission.objects.get_or_create(
 | 
				
			||||||
 | 
					                codename=self.perms_codename_format.format(obj=obj, perm=name),
 | 
				
			||||||
 | 
					                content_type=ContentType.objects.get_for_model(obj),
 | 
				
			||||||
 | 
					                defaults={"name": self.perms_name_format.format(obj=obj, perm=name)},
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            if perm not in group.permissions.all():
 | 
				
			||||||
 | 
					                group.permissions.add(perm)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					program_permissions = PagePermissions(Program)
 | 
				
			||||||
@ -1,9 +1,11 @@
 | 
				
			|||||||
from .admin import TrackSerializer, UserSettingsSerializer
 | 
					from .admin import TrackSerializer, UserSettingsSerializer
 | 
				
			||||||
from .log import LogInfo, LogInfoSerializer
 | 
					 | 
				
			||||||
from .sound import SoundSerializer
 | 
					 | 
				
			||||||
from .episode import EpisodeSoundSerializer, EpisodeSerializer
 | 
					from .episode import EpisodeSoundSerializer, EpisodeSerializer
 | 
				
			||||||
 | 
					from .log import LogInfo, LogInfoSerializer
 | 
				
			||||||
 | 
					from .page import CommentSerializer
 | 
				
			||||||
 | 
					from .sound import SoundSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
__all__ = (
 | 
					__all__ = (
 | 
				
			||||||
 | 
					    "CommentSerializer",
 | 
				
			||||||
    "LogInfo",
 | 
					    "LogInfo",
 | 
				
			||||||
    "LogInfoSerializer",
 | 
					    "LogInfoSerializer",
 | 
				
			||||||
    "EpisodeSoundSerializer",
 | 
					    "EpisodeSoundSerializer",
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										12
									
								
								aircox/serializers/page.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								aircox/serializers/page.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					from rest_framework import serializers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from aircox import models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					__all__ = ("CommentSerializer",)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CommentSerializer(serializers.ModelSerializer):
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = models.Comment
 | 
				
			||||||
 | 
					        fields = ["page", "nickname", "email", "date", "content"]
 | 
				
			||||||
@ -9834,10 +9834,34 @@ a.tag:hover {
 | 
				
			|||||||
  color: var(--secondary-color);
 | 
					  color: var(--secondary-color);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.bg-main {
 | 
				
			||||||
 | 
					  background-color: var(--main-color);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.bg-main-light {
 | 
				
			||||||
 | 
					  background-color: var(--main-color-light);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.bg-secondary {
 | 
				
			||||||
 | 
					  background-color: var(--secondary-color);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.bg-secondary-light {
 | 
				
			||||||
 | 
					  background-color: var(--secondary-color-light);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.bg-transparent {
 | 
					.bg-transparent {
 | 
				
			||||||
  background-color: transparent;
 | 
					  background-color: transparent;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.border-bottom-main {
 | 
				
			||||||
 | 
					  border-bottom: 1px solid var(--main-color);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.border-bottom-secondary {
 | 
				
			||||||
 | 
					  border-bottom: 1px solid var(--secondary-color);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.is-success {
 | 
					.is-success {
 | 
				
			||||||
  background-color: #0e0 !important;
 | 
					  background-color: #0e0 !important;
 | 
				
			||||||
  border-color: #0b0 !important;
 | 
					  border-color: #0b0 !important;
 | 
				
			||||||
 | 
				
			|||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@ -37,6 +37,7 @@ Usefull context:
 | 
				
			|||||||
        {% block head_extra %}{% endblock %}
 | 
					        {% block head_extra %}{% endblock %}
 | 
				
			||||||
    </head>
 | 
					    </head>
 | 
				
			||||||
    <body {% if request.is_mobile %}class="mobile"{% endif %}>
 | 
					    <body {% if request.is_mobile %}class="mobile"{% endif %}>
 | 
				
			||||||
 | 
					        {% block body-head %}{% endblock %}
 | 
				
			||||||
        <script id="init-script">
 | 
					        <script id="init-script">
 | 
				
			||||||
            window.addEventListener('load', function() {
 | 
					            window.addEventListener('load', function() {
 | 
				
			||||||
                {% block init-scripts %}
 | 
					                {% block init-scripts %}
 | 
				
			||||||
@ -64,7 +65,7 @@ Usefull context:
 | 
				
			|||||||
                        {% endfor %}
 | 
					                        {% endfor %}
 | 
				
			||||||
                        {% endblock %}
 | 
					                        {% endblock %}
 | 
				
			||||||
                        {% if user.is_authenticated %}
 | 
					                        {% if user.is_authenticated %}
 | 
				
			||||||
                        {% include "./dashboard/nav.html" %}
 | 
					                        {% include "./dashboard/widgets/nav.html" %}
 | 
				
			||||||
                        {% endif %}
 | 
					                        {% endif %}
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                    {% endblock %}
 | 
					                    {% endblock %}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,11 @@
 | 
				
			|||||||
{% extends "../base.html" %}
 | 
					{% extends "../base.html" %}
 | 
				
			||||||
{% load i18n %}
 | 
					{% load static i18n %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block assets %}
 | 
				
			||||||
 | 
					{{ block.super }}
 | 
				
			||||||
 | 
					<script src="{% static "aircox/js/dashboard.js" %}"></script>
 | 
				
			||||||
 | 
					<link rel="stylesheet" type="text/css" href="{% static "aircox/css/admin.css" %}"/>
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block head-title %}
 | 
					{% block head-title %}
 | 
				
			||||||
    {% block title %}{{ block.super }}{% endblock %}
 | 
					    {% block title %}{{ block.super }}{% endblock %}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										92
									
								
								aircox/templates/aircox/dashboard/statistics.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								aircox/templates/aircox/dashboard/statistics.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,92 @@
 | 
				
			|||||||
 | 
					{% extends "./base.html" %}
 | 
				
			||||||
 | 
					{% load i18n aircox %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block title %}{% translate "Statistics" %}{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block content-container %}
 | 
				
			||||||
 | 
					<div class="container">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{# TODO: date subtitle #}
 | 
				
			||||||
 | 
					{% comment %}
 | 
				
			||||||
 | 
					<nav class="navbar" role="menu">
 | 
				
			||||||
 | 
					    {% with "admin:tools-stats" as url_name %}
 | 
				
			||||||
 | 
					    {% include "aircox/widgets/dates_menu.html" %}
 | 
				
			||||||
 | 
					    {% endwith %}
 | 
				
			||||||
 | 
					</nav>
 | 
				
			||||||
 | 
					{% endcomment %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<a-statistics class="column">
 | 
				
			||||||
 | 
					<template v-slot="{counts}">
 | 
				
			||||||
 | 
					    <table class="table is-hoverable is-fullwidth">
 | 
				
			||||||
 | 
					        <thead>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					                <th>{% translate "Time" %}</th>
 | 
				
			||||||
 | 
					                <th>{% translate "Episode" %} / {% translate "Track" %}</th>
 | 
				
			||||||
 | 
					                <th>{% translate "Tags" %}</th>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					        </thead>
 | 
				
			||||||
 | 
					        <tbody>
 | 
				
			||||||
 | 
					        {% for object in object_list %}
 | 
				
			||||||
 | 
					        {% with object|is_diffusion as is_diff %}
 | 
				
			||||||
 | 
					        {% if is_diff %}
 | 
				
			||||||
 | 
					        <tr class="bg-main">
 | 
				
			||||||
 | 
					            <td>{{ object.start|time:"H:i" }} - {{ object.end|time:"H:i" }}</td>
 | 
				
			||||||
 | 
					            <td colspan="2">
 | 
				
			||||||
 | 
					                <a href="{% url "episode-detail" slug=object.episode.slug %}" target="new">{{ object.episode|default:"" }}</a>
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
 | 
					        </tr>
 | 
				
			||||||
 | 
					        {% endif %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {% with object|get_tracks as tracks %}
 | 
				
			||||||
 | 
					        {% for track in tracks %}
 | 
				
			||||||
 | 
					            <tr {% if is_diff %}class="bg-main-light"{% endif %}>
 | 
				
			||||||
 | 
					                {% if forloop.first %}
 | 
				
			||||||
 | 
					                <td rowspan="{{ tracks|length }}">{{ object.start|time:"H:i" }} {% if object|is_diffusion %} - {{ object.end|time:"H:i" }}{% endif %}</td>
 | 
				
			||||||
 | 
					                {% endif %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                    {% if object.source %}{{ object.source }} / {% endif %}
 | 
				
			||||||
 | 
					                    {% include "aircox/widgets/track_item.html" with object=track %}
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					                {% with track.tags.all|join:', ' as tags %}
 | 
				
			||||||
 | 
					                <td>
 | 
				
			||||||
 | 
					                    {% if tags and tags.strip %}
 | 
				
			||||||
 | 
					                    <label class="checkbox">
 | 
				
			||||||
 | 
					                        <input type="checkbox" checked value="{{ tags|escape }}" name="data">
 | 
				
			||||||
 | 
					                        {{ tags }}
 | 
				
			||||||
 | 
					                    </label>
 | 
				
			||||||
 | 
					                    {% endif %}
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					                {% endwith %}
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					        {% empty %}
 | 
				
			||||||
 | 
					            {% if is_diff %}
 | 
				
			||||||
 | 
					            <tr class="bg-main-light">
 | 
				
			||||||
 | 
					                <td colspan="3">{% translate "No tracks" %}</td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					            {% endif %}
 | 
				
			||||||
 | 
					        {% endfor %}
 | 
				
			||||||
 | 
					        {% endwith %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {% endwith %}
 | 
				
			||||||
 | 
					        {% endfor %}
 | 
				
			||||||
 | 
					        </tbody>
 | 
				
			||||||
 | 
					        <tfoot>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					                <td class="is-size-6">{% translate "Totals" %}</td>
 | 
				
			||||||
 | 
					                <td colspan="100">
 | 
				
			||||||
 | 
					                    <div class="columns is-size-6">
 | 
				
			||||||
 | 
					                        <span v-for="(count, tag) in counts" class="column">
 | 
				
			||||||
 | 
					                            <b>[[ tag ]]</b>
 | 
				
			||||||
 | 
					                            [[ count ]]
 | 
				
			||||||
 | 
					                        </span>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					        </tfoot>
 | 
				
			||||||
 | 
					    </table>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					</a-statistics>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
@ -10,19 +10,23 @@
 | 
				
			|||||||
    <div class="dropdown-menu" id="dropdown-menu" role="menu" style="z-index:200">
 | 
					    <div class="dropdown-menu" id="dropdown-menu" role="menu" style="z-index:200">
 | 
				
			||||||
        <div class="dropdown-content">
 | 
					        <div class="dropdown-content">
 | 
				
			||||||
            {% block user-menu %}
 | 
					            {% block user-menu %}
 | 
				
			||||||
            <a class="dropdown-item" href="{% url "dashboard" %}">
 | 
					            <a class="dropdown-item" href="{% url "dashboard" %}" data-force-reload="1">
 | 
				
			||||||
                {% translate "Dashboard" %}
 | 
					                {% translate "Dashboard" %}
 | 
				
			||||||
            </a>
 | 
					            </a>
 | 
				
			||||||
            {% endblock %}
 | 
					            {% endblock %}
 | 
				
			||||||
            {% if user.is_admin %}
 | 
					            {% if user.is_superuser %}
 | 
				
			||||||
 | 
					            <hr class="dropdown-divider" />
 | 
				
			||||||
            {% block admin-menu %}
 | 
					            {% block admin-menu %}
 | 
				
			||||||
            <a class="dropdown-item" href="{% url "admin:index" %}" target="new">
 | 
					            <a class="dropdown-item" href="{% url "admin:index" %}" target="new">
 | 
				
			||||||
                {% translate "Admin" %}
 | 
					                {% translate "Admin" %}
 | 
				
			||||||
            </a>
 | 
					            </a>
 | 
				
			||||||
 | 
					            <a class="dropdown-item" href="{% url "dashboard-statistics" %}">
 | 
				
			||||||
 | 
					                {% translate "Statistics" %}
 | 
				
			||||||
 | 
					            </a>
 | 
				
			||||||
            {% endblock %}
 | 
					            {% endblock %}
 | 
				
			||||||
            <hr class="dropdown-divider" />
 | 
					            <hr class="dropdown-divider" />
 | 
				
			||||||
            {% endif %}
 | 
					            {% endif %}
 | 
				
			||||||
            <a class="dropdown-item" href="{% url "logout" %}">
 | 
					            <a class="dropdown-item" href="{% url "logout" %}" data-force-reload="1">
 | 
				
			||||||
                {% translate "Disconnect" %}
 | 
					                {% translate "Disconnect" %}
 | 
				
			||||||
            </a>
 | 
					            </a>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
@ -6,10 +6,10 @@
 | 
				
			|||||||
    <template v-slot="{podcasts,page}">
 | 
					    <template v-slot="{podcasts,page}">
 | 
				
			||||||
        {{ block.super }}
 | 
					        {{ block.super }}
 | 
				
			||||||
        <hr/>
 | 
					        <hr/>
 | 
				
			||||||
        {% include "./dashboard/tracklist_editor.html" with formset=tracklist_formset formset_data=tracklist_formset_data %}
 | 
					        {% include "./dashboard/widgets/tracklist_editor.html" with formset=tracklist_formset formset_data=tracklist_formset_data %}
 | 
				
			||||||
        <hr/>
 | 
					        <hr/>
 | 
				
			||||||
        <h2 class="title is-2">{% translate "Podcasts" %}</h2>
 | 
					        <h2 class="title is-2">{% translate "Podcasts" %}</h2>
 | 
				
			||||||
        {% include "./dashboard/soundlist_editor.html" with formset=soundlist_formset formset_data=soundlist_formset_data %}
 | 
					        {% include "./dashboard/widgets/soundlist_editor.html" with formset=soundlist_formset formset_data=soundlist_formset_data %}
 | 
				
			||||||
    </template>
 | 
					    </template>
 | 
				
			||||||
</a-episode>
 | 
					</a-episode>
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
				
			|||||||
@ -69,7 +69,7 @@ aircox.labels = {% inline_labels %}
 | 
				
			|||||||
            {% elif field.name == "content" %}
 | 
					            {% elif field.name == "content" %}
 | 
				
			||||||
            <textarea name="{{ field.name }}" class="is-fullwidth height-25">{{ field.value|striptags|safe }}</textarea>
 | 
					            <textarea name="{{ field.name }}" class="is-fullwidth height-25">{{ field.value|striptags|safe }}</textarea>
 | 
				
			||||||
            {% else %}
 | 
					            {% else %}
 | 
				
			||||||
            {% include "./dashboard/form_field.html" with field=field.field name=field.name value=field.initial %}
 | 
					            {% include "./dashboard/widgets/form_field.html" with field=field.field name=field.name value=field.initial %}
 | 
				
			||||||
            {% endif %}
 | 
					            {% endif %}
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <p class="help">{{ field.help_text }}</p>
 | 
					        <p class="help">{{ field.help_text }}</p>
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,6 @@
 | 
				
			|||||||
{% load i18n humanize aircox %}
 | 
					{% load i18n humanize aircox %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block tag-class %}{{ block.super }} comment{% endblock %}
 | 
					{% block tag-class %}{{ block.super }} comment{% endblock %}
 | 
				
			||||||
{% block tag-extra %} id="comment-{{ object.pk }}"{% endblock %}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block outer %}
 | 
					{% block outer %}
 | 
				
			||||||
{% with url=object.get_absolute_url %}
 | 
					{% with url=object.get_absolute_url %}
 | 
				
			||||||
@ -33,18 +32,23 @@
 | 
				
			|||||||
{% block actions %}
 | 
					{% block actions %}
 | 
				
			||||||
{{ block.super }}
 | 
					{{ block.super }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% if request.user.is_staff %}
 | 
					{% if admin %}
 | 
				
			||||||
 | 
					{% if user.is_staff %}
 | 
				
			||||||
<a href="{% url "admin:aircox_comment_change" object.pk %}" class="button"
 | 
					<a href="{% url "admin:aircox_comment_change" object.pk %}" class="button"
 | 
				
			||||||
   title="{% trans "Edit comment" %}"
 | 
					   title="{% trans "Edit comment" %}"
 | 
				
			||||||
   aria-label="{% trans "Edit comment" %}">
 | 
					   aria-label="{% trans "Edit comment" %}">
 | 
				
			||||||
    <span class="fa fa-edit"></span>
 | 
					    <span class="fa fa-edit"></span>
 | 
				
			||||||
</a>
 | 
					</a>
 | 
				
			||||||
<a class="button is-danger"
 | 
					{% endif %}
 | 
				
			||||||
   title="{% trans "Delete comment" %}"
 | 
					<a-action-button class="button is-danger"
 | 
				
			||||||
   aria-label="{% trans "Delete comment" %}"
 | 
					    title="{% trans "Delete comment" %}"
 | 
				
			||||||
   href="{% url "admin:aircox_comment_delete" object.pk %}">
 | 
					    aria-label="{% trans "Delete comment" %}"
 | 
				
			||||||
    <span class="fa fa-trash-alt"></span>
 | 
					    url="{% url "api:comment-detail" object.pk %}"
 | 
				
			||||||
</a>
 | 
					    icon="fa fa-trash-alt"
 | 
				
			||||||
 | 
					    method="delete"
 | 
				
			||||||
 | 
					    confirm="{% translate "Delete comment?" %}"
 | 
				
			||||||
 | 
					    @done="deleteElements('#{{ object|object_id }}')"
 | 
				
			||||||
 | 
					   />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{# <a href="mailto:{{ object.email }}">{{ object.nickname }}</a> #}
 | 
					{# <a href="mailto:{{ object.email }}">{{ object.nickname }}</a> #}
 | 
				
			||||||
{% endif %}
 | 
					{% endif %}
 | 
				
			||||||
 | 
				
			|||||||
@ -20,9 +20,10 @@ Styling related context:
 | 
				
			|||||||
- tag_extra: extra tag attributes
 | 
					- tag_extra: extra tag attributes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% endcomment %}
 | 
					{% endcomment %}
 | 
				
			||||||
 | 
					{% load aircox %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block outer %}
 | 
					{% block outer %}
 | 
				
			||||||
<{{ tag|default:"article" }} class="preview {% if not cover %}no-cover {% endif %}{% if is_active %}active {% endif %}{% if is_tiny %}tiny{% elif is_small %}small{% endif %}{% block tag-class %}{{ tag_class|default:"" }} {% endblock %}" {% block tag-extra %}{% endblock %}>
 | 
					<{{ tag|default:"article" }} id="{{ object|object_id }}" class="preview {% if not cover %}no-cover {% endif %}{% if is_active %}active {% endif %}{% if is_tiny %}tiny{% elif is_small %}small{% endif %}{% block tag-class %}{{ tag_class|default:"" }} {% endblock %}" {% block tag-extra %}{% endblock %}>
 | 
				
			||||||
{% block inner %}
 | 
					{% block inner %}
 | 
				
			||||||
    {% block headings-container %}
 | 
					    {% block headings-container %}
 | 
				
			||||||
    <header class="headings{% block headings-class %}{% endblock %}"{% block headings-tag-extra %}{% endblock %}>
 | 
					    <header class="headings{% block headings-class %}{% endblock %}"{% block headings-tag-extra %}{% endblock %}>
 | 
				
			||||||
 | 
				
			|||||||
@ -2,6 +2,7 @@ import json
 | 
				
			|||||||
import random
 | 
					import random
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django import template, forms
 | 
					from django import template, forms
 | 
				
			||||||
 | 
					from django.db import models
 | 
				
			||||||
from django.contrib.admin.templatetags.admin_urls import admin_urlname
 | 
					from django.contrib.admin.templatetags.admin_urls import admin_urlname
 | 
				
			||||||
from django.template.loader import render_to_string
 | 
					from django.template.loader import render_to_string
 | 
				
			||||||
from django.urls import reverse
 | 
					from django.urls import reverse
 | 
				
			||||||
@ -19,6 +20,18 @@ def admin_url(obj, action):
 | 
				
			|||||||
    return reverse(f"admin:{meta.app_label}_{meta.model_name}_{action}", args=[obj.id])
 | 
					    return reverse(f"admin:{meta.app_label}_{meta.model_name}_{action}", args=[obj.id])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@register.filter(name="model_label")
 | 
				
			||||||
 | 
					def model_label(obj):
 | 
				
			||||||
 | 
					    if isinstance(obj, models.Model):
 | 
				
			||||||
 | 
					        obj = type(obj)
 | 
				
			||||||
 | 
					    return obj._meta.label_lower.replace(".", "-")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@register.filter(name="object_id")
 | 
				
			||||||
 | 
					def object_id(obj):
 | 
				
			||||||
 | 
					    return f"{model_label(obj)}-{obj.pk}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@register.simple_tag(name="page_widget", takes_context=True)
 | 
					@register.simple_tag(name="page_widget", takes_context=True)
 | 
				
			||||||
def do_page_widget(context, widget, object, dir="aircox/widgets", **ctx):
 | 
					def do_page_widget(context, widget, object, dir="aircox/widgets", **ctx):
 | 
				
			||||||
    """Render widget for the provided page and context."""
 | 
					    """Render widget for the provided page and context."""
 | 
				
			||||||
 | 
				
			|||||||
@ -24,6 +24,7 @@ router = DefaultRouter()
 | 
				
			|||||||
router.register("images", viewsets.ImageViewSet, basename="image")
 | 
					router.register("images", viewsets.ImageViewSet, basename="image")
 | 
				
			||||||
router.register("sound", viewsets.SoundViewSet, basename="sound")
 | 
					router.register("sound", viewsets.SoundViewSet, basename="sound")
 | 
				
			||||||
router.register("track", viewsets.TrackROViewSet, basename="track")
 | 
					router.register("track", viewsets.TrackROViewSet, basename="track")
 | 
				
			||||||
 | 
					router.register("comment", viewsets.CommentViewSet, basename="comment")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
api = [
 | 
					api = [
 | 
				
			||||||
@ -121,6 +122,8 @@ urls = [
 | 
				
			|||||||
    path(_("dashboard/"), views.dashboard.DashboardView.as_view(), name="dashboard"),
 | 
					    path(_("dashboard/"), views.dashboard.DashboardView.as_view(), name="dashboard"),
 | 
				
			||||||
    path(_("dashboard/program/<pk>/"), views.ProgramUpdateView.as_view(), name="program-edit"),
 | 
					    path(_("dashboard/program/<pk>/"), views.ProgramUpdateView.as_view(), name="program-edit"),
 | 
				
			||||||
    path(_("dashboard/episodes/<pk>/"), views.EpisodeUpdateView.as_view(), name="episode-edit"),
 | 
					    path(_("dashboard/episodes/<pk>/"), views.EpisodeUpdateView.as_view(), name="episode-edit"),
 | 
				
			||||||
 | 
					    path(_("dashboard/statistics/"), views.dashboard.StatisticsView.as_view(), name="dashboard-statistics"),
 | 
				
			||||||
 | 
					    path(_("dashboard/statistics/<date:date>/"), views.dashboard.StatisticsView.as_view(), name="dashboard-statistics"),
 | 
				
			||||||
    # ---- others
 | 
					    # ---- others
 | 
				
			||||||
    path("errors/no-station/", views.errors.NoStationErrorView.as_view(), name="errors-no-station"),
 | 
					    path("errors/no-station/", views.errors.NoStationErrorView.as_view(), name="errors-no-station"),
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
				
			|||||||
@ -31,7 +31,7 @@ class AdminMixin(LoginRequiredMixin, UserPassesTestMixin):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class StatisticsView(AdminMixin, LogListView, ListView):
 | 
					class StatisticsView(AdminMixin, LogListView, ListView):
 | 
				
			||||||
    template_name = "admin/aircox/statistics.html"
 | 
					    template_name = "admin/aircox/statistics.html"
 | 
				
			||||||
    redirect_date_url = "admin:tools-stats"
 | 
					    # redirect_date_url = "admin:tools-stats"
 | 
				
			||||||
    title = _("Statistics")
 | 
					    title = _("Statistics")
 | 
				
			||||||
    date = None
 | 
					    date = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -4,13 +4,21 @@ from django.utils.translation import gettext_lazy as _
 | 
				
			|||||||
from django.views.generic.base import TemplateView
 | 
					from django.views.generic.base import TemplateView
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from aircox import models
 | 
					from aircox import models
 | 
				
			||||||
 | 
					from aircox.controllers.log_archiver import LogArchiver
 | 
				
			||||||
from .base import BaseView
 | 
					from .base import BaseView
 | 
				
			||||||
 | 
					from .log import LogListView
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class DashboardView(LoginRequiredMixin, BaseView, TemplateView):
 | 
					__all__ = ("DashboardBaseView", "DashboardView", "StatisticsView")
 | 
				
			||||||
    template_name = "aircox/dashboard/dashboard.html"
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DashboardBaseView(LoginRequiredMixin, BaseView):
 | 
				
			||||||
    title = _("Dashboard")
 | 
					    title = _("Dashboard")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DashboardView(DashboardBaseView, TemplateView):
 | 
				
			||||||
 | 
					    template_name = "aircox/dashboard/dashboard.html"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        programs = models.Program.objects.editor(self.request.user)
 | 
					        programs = models.Program.objects.editor(self.request.user)
 | 
				
			||||||
        comments = models.Comment.objects.filter(
 | 
					        comments = models.Comment.objects.filter(
 | 
				
			||||||
@ -29,3 +37,17 @@ class DashboardView(LoginRequiredMixin, BaseView, TemplateView):
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        return super().get_context_data(**kwargs)
 | 
					        return super().get_context_data(**kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class StatisticsView(DashboardBaseView, LogListView):
 | 
				
			||||||
 | 
					    template_name = "aircox/dashboard/statistics.html"
 | 
				
			||||||
 | 
					    date = None
 | 
				
			||||||
 | 
					    redirect_date_url = "dashboard-statistics"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # TOOD: test_func & perms check
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_object_list(self, logs, full=False):
 | 
				
			||||||
 | 
					        if not logs.exists():
 | 
				
			||||||
 | 
					            logs = LogArchiver().load(self.station, self.date) if self.date else []
 | 
				
			||||||
 | 
					        objs = super().get_object_list(logs, True)
 | 
				
			||||||
 | 
					        return objs
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@ from django.contrib.auth.mixins import UserPassesTestMixin
 | 
				
			|||||||
from django.urls import reverse
 | 
					from django.urls import reverse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from aircox.models import Episode, Program, StaticPage, Track
 | 
					from aircox.models import Episode, Program, StaticPage, Track
 | 
				
			||||||
from aircox import forms, filters
 | 
					from aircox import forms, filters, permissions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .mixins import VueFormDataMixin
 | 
					from .mixins import VueFormDataMixin
 | 
				
			||||||
from .page import PageDetailView, PageListView, PageUpdateView
 | 
					from .page import PageDetailView, PageListView, PageUpdateView
 | 
				
			||||||
@ -51,8 +51,8 @@ class EpisodeUpdateView(UserPassesTestMixin, VueFormDataMixin, PageUpdateView):
 | 
				
			|||||||
    template_name = "aircox/episode_form.html"
 | 
					    template_name = "aircox/episode_form.html"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_func(self):
 | 
					    def test_func(self):
 | 
				
			||||||
        obj = self.get_object().parent_subclass
 | 
					        obj = self.get_object()
 | 
				
			||||||
        return obj.can_update(self.request.user)
 | 
					        return permissions.program_permissions.can(self.request.user, "update", obj)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_tracklist_queryset(self, episode):
 | 
					    def get_tracklist_queryset(self, episode):
 | 
				
			||||||
        return Track.objects.filter(episode=episode).order_by("position")
 | 
					        return Track.objects.filter(episode=episode).order_by("position")
 | 
				
			||||||
 | 
				
			|||||||
@ -55,6 +55,7 @@ class LogListMixin(GetDateMixin):
 | 
				
			|||||||
        diffs = self.get_diffusions_queryset()
 | 
					        diffs = self.get_diffusions_queryset()
 | 
				
			||||||
        if self.request.user.is_staff and full:
 | 
					        if self.request.user.is_staff and full:
 | 
				
			||||||
            return sorted(list(logs) + list(diffs), key=lambda obj: obj.start)
 | 
					            return sorted(list(logs) + list(diffs), key=lambda obj: obj.start)
 | 
				
			||||||
 | 
					        print(">>>>", len(logs), len(diffs), Log.merge_diffusions(logs, diffs))
 | 
				
			||||||
        return Log.merge_diffusions(logs, diffs)
 | 
					        return Log.merge_diffusions(logs, diffs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -3,8 +3,7 @@ import random
 | 
				
			|||||||
from django.contrib.auth.mixins import UserPassesTestMixin
 | 
					from django.contrib.auth.mixins import UserPassesTestMixin
 | 
				
			||||||
from django.urls import reverse
 | 
					from django.urls import reverse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from aircox.forms import ProgramForm
 | 
					from aircox import models, forms, permissions
 | 
				
			||||||
from aircox.models import Article, Episode, Program, StaticPage
 | 
					 | 
				
			||||||
from .page import PageDetailView, PageListView, PageUpdateView
 | 
					from .page import PageDetailView, PageListView, PageUpdateView
 | 
				
			||||||
 | 
					
 | 
				
			||||||
__all__ = (
 | 
					__all__ = (
 | 
				
			||||||
@ -14,7 +13,7 @@ __all__ = (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ProgramDetailView(PageDetailView):
 | 
					class ProgramDetailView(PageDetailView):
 | 
				
			||||||
    model = Program
 | 
					    model = models.Program
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_related_queryset(self):
 | 
					    def get_related_queryset(self):
 | 
				
			||||||
        queryset = (
 | 
					        queryset = (
 | 
				
			||||||
@ -30,9 +29,9 @@ class ProgramDetailView(PageDetailView):
 | 
				
			|||||||
        return reverse("program-list") + f"?category__id={self.object.category_id}"
 | 
					        return reverse("program-list") + f"?category__id={self.object.category_id}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        episodes = Episode.objects.program(self.object).published().order_by("-pub_date")
 | 
					        episodes = models.Episode.objects.program(self.object).published().order_by("-pub_date")
 | 
				
			||||||
        podcasts = episodes.with_podcasts()
 | 
					        podcasts = episodes.with_podcasts()
 | 
				
			||||||
        articles = Article.objects.parent(self.object).published().order_by("-pub_date")
 | 
					        articles = models.Article.objects.parent(self.object).published().order_by("-pub_date")
 | 
				
			||||||
        return super().get_context_data(
 | 
					        return super().get_context_data(
 | 
				
			||||||
            articles=articles[: self.related_count],
 | 
					            articles=articles[: self.related_count],
 | 
				
			||||||
            episodes=episodes[: self.related_count],
 | 
					            episodes=episodes[: self.related_count],
 | 
				
			||||||
@ -45,20 +44,20 @@ class ProgramDetailView(PageDetailView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ProgramListView(PageListView):
 | 
					class ProgramListView(PageListView):
 | 
				
			||||||
    model = Program
 | 
					    model = models.Program
 | 
				
			||||||
    attach_to_value = StaticPage.Target.PROGRAMS
 | 
					    attach_to_value = models.StaticPage.Target.PROGRAMS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_queryset(self):
 | 
					    def get_queryset(self):
 | 
				
			||||||
        return super().get_queryset().order_by("title")
 | 
					        return super().get_queryset().order_by("title")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ProgramUpdateView(UserPassesTestMixin, PageUpdateView):
 | 
					class ProgramUpdateView(UserPassesTestMixin, PageUpdateView):
 | 
				
			||||||
    model = Program
 | 
					    model = models.Program
 | 
				
			||||||
    form_class = ProgramForm
 | 
					    form_class = forms.ProgramForm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_func(self):
 | 
					    def test_func(self):
 | 
				
			||||||
        obj = self.get_object()
 | 
					        obj = self.get_object()
 | 
				
			||||||
        return obj.can_update(self.request.user)
 | 
					        return permissions.program_permissions.can(self.request.user, "update", obj)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_success_url(self):
 | 
					    def get_success_url(self):
 | 
				
			||||||
        return reverse("program-detail", kwargs={"slug": self.get_object().slug})
 | 
					        return reverse("program-detail", kwargs={"slug": self.get_object().slug})
 | 
				
			||||||
 | 
				
			|||||||
@ -74,6 +74,12 @@ class TrackROViewSet(viewsets.ReadOnlyModelViewSet):
 | 
				
			|||||||
        return self.list(request)
 | 
					        return self.list(request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CommentViewSet(viewsets.ModelViewSet):
 | 
				
			||||||
 | 
					    serializer_class = serializers.CommentSerializer
 | 
				
			||||||
 | 
					    permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
 | 
				
			||||||
 | 
					    queryset = models.Comment.objects.all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# --- admin
 | 
					# --- admin
 | 
				
			||||||
class UserSettingsViewSet(viewsets.ViewSet):
 | 
					class UserSettingsViewSet(viewsets.ViewSet):
 | 
				
			||||||
    """User's settings specific to aircox.
 | 
					    """User's settings specific to aircox.
 | 
				
			||||||
 | 
				
			|||||||
@ -15,6 +15,13 @@ const App = {
 | 
				
			|||||||
    computed: {
 | 
					    computed: {
 | 
				
			||||||
        player() { return window.aircox.player; },
 | 
					        player() { return window.aircox.player; },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    methods: {
 | 
				
			||||||
 | 
					        deleteElements(sel) {
 | 
				
			||||||
 | 
					            for(var el of document.querySelectorAll(sel))
 | 
				
			||||||
 | 
					                el.parentNode.removeChild(el)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const PlayerApp = {
 | 
					export const PlayerApp = {
 | 
				
			||||||
 | 
				
			|||||||
@ -70,11 +70,11 @@ export default {
 | 
				
			|||||||
                method: this.method,
 | 
					                method: this.method,
 | 
				
			||||||
                body: JSON.stringify(this.item.data),
 | 
					                body: JSON.stringify(this.item.data),
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
            this.promise = fetch(this.url, options).then(data => {
 | 
					            this.promise = fetch(this.url, options).then(data => data.text()).then(data => {
 | 
				
			||||||
                const response = data.json();
 | 
					                data = data && JSON.parse(data) || null
 | 
				
			||||||
                this.promise = null;
 | 
					                this.promise = null;
 | 
				
			||||||
                this.$emit('done', response)
 | 
					                this.$emit('done', data)
 | 
				
			||||||
                return response
 | 
					                return data
 | 
				
			||||||
            }, data => { this.promise = null; return data })
 | 
					            }, data => { this.promise = null; return data })
 | 
				
			||||||
            return this.promise
 | 
					            return this.promise
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
				
			|||||||
@ -5,7 +5,7 @@
 | 
				
			|||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
const splitReg = new RegExp(',\\s*', 'g');
 | 
					const splitReg = new RegExp(',\\s*|\\s+', 'g');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
    data() {
 | 
					    data() {
 | 
				
			||||||
@ -22,7 +22,8 @@ export default {
 | 
				
			|||||||
            for(var item of items)
 | 
					            for(var item of items)
 | 
				
			||||||
                if(item.value)
 | 
					                if(item.value)
 | 
				
			||||||
                    for(var tag of item.value.split(splitReg))
 | 
					                    for(var tag of item.value.split(splitReg))
 | 
				
			||||||
                        counts[tag.trim()] = (counts[tag.trim()] || 0) + 1;
 | 
					                        if(tag.trim())
 | 
				
			||||||
 | 
					                            counts[tag.trim()] = (counts[tag.trim()] || 0) + 1;
 | 
				
			||||||
            this.counts = counts;
 | 
					            this.counts = counts;
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -34,11 +34,12 @@ export default base
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const admin = {
 | 
					export const admin = {
 | 
				
			||||||
    ...base,
 | 
					    ...base,
 | 
				
			||||||
    AStatistics, AStreamer, ATrackListEditor
 | 
					    ATrackListEditor
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const dashboard = {
 | 
					export const dashboard = {
 | 
				
			||||||
    ...base,
 | 
					    ...base,
 | 
				
			||||||
    AActionButton, AFileUpload, ASelectFile, AModal,
 | 
					    AActionButton, AFileUpload, ASelectFile, AModal,
 | 
				
			||||||
    AFormSet, ATrackListEditor, ASoundListEditor
 | 
					    AFormSet, ATrackListEditor, ASoundListEditor,
 | 
				
			||||||
 | 
					    AStatistics, AStreamer,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -141,7 +141,7 @@ export default class PageLoad {
 | 
				
			|||||||
        let submit = event.type == 'submit';
 | 
					        let submit = event.type == 'submit';
 | 
				
			||||||
        let target = submit || event.target.tagName == 'A'
 | 
					        let target = submit || event.target.tagName == 'A'
 | 
				
			||||||
                        ? event.target : event.target.closest('a');
 | 
					                        ? event.target : event.target.closest('a');
 | 
				
			||||||
        if(!target || target.hasAttribute('target'))
 | 
					        if(!target || target.hasAttribute('target') || target.data.forceReload)
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let url = submit ? target.getAttribute('action') || ''
 | 
					        let url = submit ? target.getAttribute('action') || ''
 | 
				
			||||||
 | 
				
			|||||||
@ -123,8 +123,15 @@
 | 
				
			|||||||
.main-color { color: var(--main-color); }
 | 
					.main-color { color: var(--main-color); }
 | 
				
			||||||
.secondary-color { color: var(--secondary-color); }
 | 
					.secondary-color { color: var(--secondary-color); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.bg-main { background-color: var(--main-color); }
 | 
				
			||||||
 | 
					.bg-main-light { background-color: var(--main-color-light); }
 | 
				
			||||||
 | 
					.bg-secondary { background-color: var(--secondary-color); }
 | 
				
			||||||
 | 
					.bg-secondary-light { background-color: var(--secondary-color-light); }
 | 
				
			||||||
.bg-transparent { background-color: transparent; }
 | 
					.bg-transparent { background-color: transparent; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.border-bottom-main { border-bottom: 1px solid var(--main-color); }
 | 
				
			||||||
 | 
					.border-bottom-secondary { border-bottom: 1px solid var(--secondary-color); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.is-success {
 | 
					.is-success {
 | 
				
			||||||
    background-color: v.$green !important;
 | 
					    background-color: v.$green !important;
 | 
				
			||||||
    border-color: v.$green-dark !important;
 | 
					    border-color: v.$green-dark !important;
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user