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
|
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;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user