integrate statistics
This commit is contained in:
parent
7841fed17d
commit
0ee72f30c5
|
@ -4,6 +4,14 @@ from django.db import migrations, models
|
|||
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):
|
||||
dependencies = [
|
||||
("auth", "0012_alter_user_first_name_max_length"),
|
||||
|
@ -22,4 +30,5 @@ class Migration(migrations.Migration):
|
|||
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()
|
||||
|
||||
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:
|
||||
verbose_name = _("Comment")
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import os
|
||||
|
||||
from django.conf import settings as conf
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.auth.models import Group
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
@ -117,37 +116,6 @@ class Program(Page):
|
|||
os.makedirs(path, exist_ok=True)
|
||||
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:
|
||||
verbose_name = _("Program")
|
||||
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 .log import LogInfo, LogInfoSerializer
|
||||
from .sound import SoundSerializer
|
||||
from .episode import EpisodeSoundSerializer, EpisodeSerializer
|
||||
from .log import LogInfo, LogInfoSerializer
|
||||
from .page import CommentSerializer
|
||||
from .sound import SoundSerializer
|
||||
|
||||
__all__ = (
|
||||
"CommentSerializer",
|
||||
"LogInfo",
|
||||
"LogInfoSerializer",
|
||||
"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);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.border-bottom-main {
|
||||
border-bottom: 1px solid var(--main-color);
|
||||
}
|
||||
|
||||
.border-bottom-secondary {
|
||||
border-bottom: 1px solid var(--secondary-color);
|
||||
}
|
||||
|
||||
.is-success {
|
||||
background-color: #0e0 !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 %}
|
||||
</head>
|
||||
<body {% if request.is_mobile %}class="mobile"{% endif %}>
|
||||
{% block body-head %}{% endblock %}
|
||||
<script id="init-script">
|
||||
window.addEventListener('load', function() {
|
||||
{% block init-scripts %}
|
||||
|
@ -64,7 +65,7 @@ Usefull context:
|
|||
{% endfor %}
|
||||
{% endblock %}
|
||||
{% if user.is_authenticated %}
|
||||
{% include "./dashboard/nav.html" %}
|
||||
{% include "./dashboard/widgets/nav.html" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
{% 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 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-content">
|
||||
{% block user-menu %}
|
||||
<a class="dropdown-item" href="{% url "dashboard" %}">
|
||||
<a class="dropdown-item" href="{% url "dashboard" %}" data-force-reload="1">
|
||||
{% translate "Dashboard" %}
|
||||
</a>
|
||||
{% endblock %}
|
||||
{% if user.is_admin %}
|
||||
{% if user.is_superuser %}
|
||||
<hr class="dropdown-divider" />
|
||||
{% block admin-menu %}
|
||||
<a class="dropdown-item" href="{% url "admin:index" %}" target="new">
|
||||
{% translate "Admin" %}
|
||||
</a>
|
||||
<a class="dropdown-item" href="{% url "dashboard-statistics" %}">
|
||||
{% translate "Statistics" %}
|
||||
</a>
|
||||
{% endblock %}
|
||||
<hr class="dropdown-divider" />
|
||||
{% endif %}
|
||||
<a class="dropdown-item" href="{% url "logout" %}">
|
||||
<a class="dropdown-item" href="{% url "logout" %}" data-force-reload="1">
|
||||
{% translate "Disconnect" %}
|
||||
</a>
|
||||
</div>
|
|
@ -6,10 +6,10 @@
|
|||
<template v-slot="{podcasts,page}">
|
||||
{{ block.super }}
|
||||
<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/>
|
||||
<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>
|
||||
</a-episode>
|
||||
{% endblock %}
|
||||
|
|
|
@ -69,7 +69,7 @@ aircox.labels = {% inline_labels %}
|
|||
{% elif field.name == "content" %}
|
||||
<textarea name="{{ field.name }}" class="is-fullwidth height-25">{{ field.value|striptags|safe }}</textarea>
|
||||
{% 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 %}
|
||||
</div>
|
||||
<p class="help">{{ field.help_text }}</p>
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
{% load i18n humanize aircox %}
|
||||
|
||||
{% block tag-class %}{{ block.super }} comment{% endblock %}
|
||||
{% block tag-extra %} id="comment-{{ object.pk }}"{% endblock %}
|
||||
|
||||
{% block outer %}
|
||||
{% with url=object.get_absolute_url %}
|
||||
|
@ -33,18 +32,23 @@
|
|||
{% block actions %}
|
||||
{{ block.super }}
|
||||
|
||||
{% if request.user.is_staff %}
|
||||
{% if admin %}
|
||||
{% if user.is_staff %}
|
||||
<a href="{% url "admin:aircox_comment_change" object.pk %}" class="button"
|
||||
title="{% trans "Edit comment" %}"
|
||||
aria-label="{% trans "Edit comment" %}">
|
||||
<span class="fa fa-edit"></span>
|
||||
</a>
|
||||
<a class="button is-danger"
|
||||
title="{% trans "Delete comment" %}"
|
||||
aria-label="{% trans "Delete comment" %}"
|
||||
href="{% url "admin:aircox_comment_delete" object.pk %}">
|
||||
<span class="fa fa-trash-alt"></span>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a-action-button class="button is-danger"
|
||||
title="{% trans "Delete comment" %}"
|
||||
aria-label="{% trans "Delete comment" %}"
|
||||
url="{% url "api:comment-detail" object.pk %}"
|
||||
icon="fa fa-trash-alt"
|
||||
method="delete"
|
||||
confirm="{% translate "Delete comment?" %}"
|
||||
@done="deleteElements('#{{ object|object_id }}')"
|
||||
/>
|
||||
|
||||
{# <a href="mailto:{{ object.email }}">{{ object.nickname }}</a> #}
|
||||
{% endif %}
|
||||
|
|
|
@ -20,9 +20,10 @@ Styling related context:
|
|||
- tag_extra: extra tag attributes
|
||||
|
||||
{% endcomment %}
|
||||
{% load aircox %}
|
||||
|
||||
{% 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 headings-container %}
|
||||
<header class="headings{% block headings-class %}{% endblock %}"{% block headings-tag-extra %}{% endblock %}>
|
||||
|
|
|
@ -2,6 +2,7 @@ import json
|
|||
import random
|
||||
|
||||
from django import template, forms
|
||||
from django.db import models
|
||||
from django.contrib.admin.templatetags.admin_urls import admin_urlname
|
||||
from django.template.loader import render_to_string
|
||||
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])
|
||||
|
||||
|
||||
@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)
|
||||
def do_page_widget(context, widget, object, dir="aircox/widgets", **ctx):
|
||||
"""Render widget for the provided page and context."""
|
||||
|
|
|
@ -24,6 +24,7 @@ router = DefaultRouter()
|
|||
router.register("images", viewsets.ImageViewSet, basename="image")
|
||||
router.register("sound", viewsets.SoundViewSet, basename="sound")
|
||||
router.register("track", viewsets.TrackROViewSet, basename="track")
|
||||
router.register("comment", viewsets.CommentViewSet, basename="comment")
|
||||
|
||||
|
||||
api = [
|
||||
|
@ -121,6 +122,8 @@ urls = [
|
|||
path(_("dashboard/"), views.dashboard.DashboardView.as_view(), name="dashboard"),
|
||||
path(_("dashboard/program/<pk>/"), views.ProgramUpdateView.as_view(), name="program-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
|
||||
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):
|
||||
template_name = "admin/aircox/statistics.html"
|
||||
redirect_date_url = "admin:tools-stats"
|
||||
# redirect_date_url = "admin:tools-stats"
|
||||
title = _("Statistics")
|
||||
date = None
|
||||
|
||||
|
|
|
@ -4,13 +4,21 @@ from django.utils.translation import gettext_lazy as _
|
|||
from django.views.generic.base import TemplateView
|
||||
|
||||
from aircox import models
|
||||
from aircox.controllers.log_archiver import LogArchiver
|
||||
from .base import BaseView
|
||||
from .log import LogListView
|
||||
|
||||
|
||||
class DashboardView(LoginRequiredMixin, BaseView, TemplateView):
|
||||
template_name = "aircox/dashboard/dashboard.html"
|
||||
__all__ = ("DashboardBaseView", "DashboardView", "StatisticsView")
|
||||
|
||||
|
||||
class DashboardBaseView(LoginRequiredMixin, BaseView):
|
||||
title = _("Dashboard")
|
||||
|
||||
|
||||
class DashboardView(DashboardBaseView, TemplateView):
|
||||
template_name = "aircox/dashboard/dashboard.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
programs = models.Program.objects.editor(self.request.user)
|
||||
comments = models.Comment.objects.filter(
|
||||
|
@ -29,3 +37,17 @@ class DashboardView(LoginRequiredMixin, BaseView, TemplateView):
|
|||
}
|
||||
)
|
||||
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 aircox.models import Episode, Program, StaticPage, Track
|
||||
from aircox import forms, filters
|
||||
from aircox import forms, filters, permissions
|
||||
|
||||
from .mixins import VueFormDataMixin
|
||||
from .page import PageDetailView, PageListView, PageUpdateView
|
||||
|
@ -51,8 +51,8 @@ class EpisodeUpdateView(UserPassesTestMixin, VueFormDataMixin, PageUpdateView):
|
|||
template_name = "aircox/episode_form.html"
|
||||
|
||||
def test_func(self):
|
||||
obj = self.get_object().parent_subclass
|
||||
return obj.can_update(self.request.user)
|
||||
obj = self.get_object()
|
||||
return permissions.program_permissions.can(self.request.user, "update", obj)
|
||||
|
||||
def get_tracklist_queryset(self, episode):
|
||||
return Track.objects.filter(episode=episode).order_by("position")
|
||||
|
|
|
@ -55,6 +55,7 @@ class LogListMixin(GetDateMixin):
|
|||
diffs = self.get_diffusions_queryset()
|
||||
if self.request.user.is_staff and full:
|
||||
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)
|
||||
|
||||
|
||||
|
|
|
@ -3,8 +3,7 @@ import random
|
|||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
from django.urls import reverse
|
||||
|
||||
from aircox.forms import ProgramForm
|
||||
from aircox.models import Article, Episode, Program, StaticPage
|
||||
from aircox import models, forms, permissions
|
||||
from .page import PageDetailView, PageListView, PageUpdateView
|
||||
|
||||
__all__ = (
|
||||
|
@ -14,7 +13,7 @@ __all__ = (
|
|||
|
||||
|
||||
class ProgramDetailView(PageDetailView):
|
||||
model = Program
|
||||
model = models.Program
|
||||
|
||||
def get_related_queryset(self):
|
||||
queryset = (
|
||||
|
@ -30,9 +29,9 @@ class ProgramDetailView(PageDetailView):
|
|||
return reverse("program-list") + f"?category__id={self.object.category_id}"
|
||||
|
||||
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()
|
||||
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(
|
||||
articles=articles[: self.related_count],
|
||||
episodes=episodes[: self.related_count],
|
||||
|
@ -45,20 +44,20 @@ class ProgramDetailView(PageDetailView):
|
|||
|
||||
|
||||
class ProgramListView(PageListView):
|
||||
model = Program
|
||||
attach_to_value = StaticPage.Target.PROGRAMS
|
||||
model = models.Program
|
||||
attach_to_value = models.StaticPage.Target.PROGRAMS
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().order_by("title")
|
||||
|
||||
|
||||
class ProgramUpdateView(UserPassesTestMixin, PageUpdateView):
|
||||
model = Program
|
||||
form_class = ProgramForm
|
||||
model = models.Program
|
||||
form_class = forms.ProgramForm
|
||||
|
||||
def test_func(self):
|
||||
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):
|
||||
return reverse("program-detail", kwargs={"slug": self.get_object().slug})
|
||||
|
|
|
@ -74,6 +74,12 @@ class TrackROViewSet(viewsets.ReadOnlyModelViewSet):
|
|||
return self.list(request)
|
||||
|
||||
|
||||
class CommentViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = serializers.CommentSerializer
|
||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
|
||||
queryset = models.Comment.objects.all()
|
||||
|
||||
|
||||
# --- admin
|
||||
class UserSettingsViewSet(viewsets.ViewSet):
|
||||
"""User's settings specific to aircox.
|
||||
|
|
|
@ -15,6 +15,13 @@ const App = {
|
|||
computed: {
|
||||
player() { return window.aircox.player; },
|
||||
},
|
||||
|
||||
methods: {
|
||||
deleteElements(sel) {
|
||||
for(var el of document.querySelectorAll(sel))
|
||||
el.parentNode.removeChild(el)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const PlayerApp = {
|
||||
|
|
|
@ -70,11 +70,11 @@ export default {
|
|||
method: this.method,
|
||||
body: JSON.stringify(this.item.data),
|
||||
})
|
||||
this.promise = fetch(this.url, options).then(data => {
|
||||
const response = data.json();
|
||||
this.promise = fetch(this.url, options).then(data => data.text()).then(data => {
|
||||
data = data && JSON.parse(data) || null
|
||||
this.promise = null;
|
||||
this.$emit('done', response)
|
||||
return response
|
||||
this.$emit('done', data)
|
||||
return data
|
||||
}, data => { this.promise = null; return data })
|
||||
return this.promise
|
||||
},
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
const splitReg = new RegExp(',\\s*', 'g');
|
||||
const splitReg = new RegExp(',\\s*|\\s+', 'g');
|
||||
|
||||
export default {
|
||||
data() {
|
||||
|
@ -22,7 +22,8 @@ export default {
|
|||
for(var item of items)
|
||||
if(item.value)
|
||||
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;
|
||||
},
|
||||
|
||||
|
|
|
@ -34,11 +34,12 @@ export default base
|
|||
|
||||
export const admin = {
|
||||
...base,
|
||||
AStatistics, AStreamer, ATrackListEditor
|
||||
ATrackListEditor
|
||||
}
|
||||
|
||||
export const dashboard = {
|
||||
...base,
|
||||
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 target = submit || event.target.tagName == 'A'
|
||||
? event.target : event.target.closest('a');
|
||||
if(!target || target.hasAttribute('target'))
|
||||
if(!target || target.hasAttribute('target') || target.data.forceReload)
|
||||
return;
|
||||
|
||||
let url = submit ? target.getAttribute('action') || ''
|
||||
|
|
|
@ -123,8 +123,15 @@
|
|||
.main-color { color: var(--main-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; }
|
||||
|
||||
.border-bottom-main { border-bottom: 1px solid var(--main-color); }
|
||||
.border-bottom-secondary { border-bottom: 1px solid var(--secondary-color); }
|
||||
|
||||
.is-success {
|
||||
background-color: v.$green !important;
|
||||
border-color: v.$green-dark !important;
|
||||
|
|
Loading…
Reference in New Issue
Block a user