rm file
This commit is contained in:
parent
1d321a0de6
commit
07d72d799d
|
@ -233,12 +233,12 @@ class SoundMonitor:
|
||||||
if not program.ensure_dir(subdir):
|
if not program.ensure_dir(subdir):
|
||||||
return
|
return
|
||||||
|
|
||||||
subdir = os.path.join(program.abspath, subdir)
|
abs_subdir = os.path.join(program.abspath, subdir)
|
||||||
sounds = []
|
sounds = []
|
||||||
|
|
||||||
# sounds in directory
|
# sounds in directory
|
||||||
for path in os.listdir(subdir):
|
for path in os.listdir(abs_subdir):
|
||||||
path = os.path.join(subdir, path)
|
path = os.path.join(abs_subdir, path)
|
||||||
if not path.endswith(settings.SOUND_FILE_EXT):
|
if not path.endswith(settings.SOUND_FILE_EXT):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -247,7 +247,7 @@ class SoundMonitor:
|
||||||
sounds.append(sound_file.sound.pk)
|
sounds.append(sound_file.sound.pk)
|
||||||
|
|
||||||
# sounds in db & unchecked
|
# sounds in db & unchecked
|
||||||
sounds = Sound.objects.filter(file__startswith=subdir).exclude(pk__in=sounds)
|
sounds = Sound.objects.filter(file__startswith=program.path).exclude(pk__in=sounds)
|
||||||
self.check_sounds(sounds, program=program)
|
self.check_sounds(sounds, program=program)
|
||||||
|
|
||||||
def check_sounds(self, qs, **sync_kwargs):
|
def check_sounds(self, qs, **sync_kwargs):
|
||||||
|
|
102
aircox/forms.py
102
aircox/forms.py
|
@ -1,102 +0,0 @@
|
||||||
from django import forms
|
|
||||||
from django.forms.models import modelformset_factory
|
|
||||||
|
|
||||||
|
|
||||||
from aircox import models
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ("CommentForm", "PageForm", "ProgramForm", "EpisodeForm", "SoundForm", "EpisodeSoundFormSet", "TrackFormSet")
|
|
||||||
|
|
||||||
|
|
||||||
class CommentForm(forms.ModelForm):
|
|
||||||
nickname = forms.CharField()
|
|
||||||
email = forms.EmailField(required=False)
|
|
||||||
content = forms.CharField(widget=forms.Textarea())
|
|
||||||
|
|
||||||
nickname.widget.attrs.update({"class": "input"})
|
|
||||||
email.widget.attrs.update({"class": "input"})
|
|
||||||
content.widget.attrs.update({"class": "textarea"})
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = models.Comment
|
|
||||||
fields = ["nickname", "email", "content"]
|
|
||||||
|
|
||||||
|
|
||||||
class ImageForm(forms.Form):
|
|
||||||
file = forms.ImageField()
|
|
||||||
|
|
||||||
|
|
||||||
class PageForm(forms.ModelForm):
|
|
||||||
class Meta:
|
|
||||||
fields = ("title", "category", "status", "cover", "content")
|
|
||||||
model = models.Page
|
|
||||||
|
|
||||||
|
|
||||||
class ChildPageForm(forms.ModelForm):
|
|
||||||
class Meta:
|
|
||||||
fields = ("title", "status", "cover", "content")
|
|
||||||
model = models.Page
|
|
||||||
|
|
||||||
|
|
||||||
class ProgramForm(PageForm):
|
|
||||||
class Meta:
|
|
||||||
fields = PageForm.Meta.fields
|
|
||||||
model = models.Program
|
|
||||||
|
|
||||||
|
|
||||||
class EpisodeForm(PageForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.Episode
|
|
||||||
fields = ChildPageForm.Meta.fields
|
|
||||||
|
|
||||||
|
|
||||||
class SoundForm(forms.ModelForm):
|
|
||||||
"""SoundForm used in EpisodeUpdateView."""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = models.Sound
|
|
||||||
fields = ["name", "program", "file", "broadcast", "duration", "is_public", "is_downloadable"]
|
|
||||||
|
|
||||||
|
|
||||||
class SoundCreateForm(forms.ModelForm):
|
|
||||||
"""SoundForm used in EpisodeUpdateView."""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = models.Sound
|
|
||||||
fields = ["name", "program", "file", "broadcast", "is_public", "is_downloadable"]
|
|
||||||
widgets = {"program": forms.HiddenInput()}
|
|
||||||
|
|
||||||
|
|
||||||
TrackFormSet = modelformset_factory(
|
|
||||||
models.Track,
|
|
||||||
fields=[
|
|
||||||
"position",
|
|
||||||
"episode",
|
|
||||||
"artist",
|
|
||||||
"title",
|
|
||||||
"tags",
|
|
||||||
],
|
|
||||||
widgets={"episode": forms.HiddenInput(), "position": forms.HiddenInput()},
|
|
||||||
can_delete=True,
|
|
||||||
extra=0,
|
|
||||||
)
|
|
||||||
"""Track formset used in EpisodeUpdateView."""
|
|
||||||
|
|
||||||
|
|
||||||
EpisodeSoundFormSet = modelformset_factory(
|
|
||||||
models.EpisodeSound,
|
|
||||||
fields=(
|
|
||||||
"position",
|
|
||||||
"episode",
|
|
||||||
"sound",
|
|
||||||
"broadcast",
|
|
||||||
),
|
|
||||||
widgets={
|
|
||||||
"broadcast": forms.CheckboxInput(),
|
|
||||||
"episode": forms.HiddenInput(),
|
|
||||||
# "sound": forms.HiddenInput(),
|
|
||||||
"position": forms.HiddenInput(),
|
|
||||||
},
|
|
||||||
can_delete=True,
|
|
||||||
extra=0,
|
|
||||||
)
|
|
23
aircox/forms/__init__.py
Normal file
23
aircox/forms/__init__.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
from . import widgets
|
||||||
|
|
||||||
|
from .auth import UserGroupFormSet
|
||||||
|
from .episode import EpisodeForm, EpisodeSoundFormSet
|
||||||
|
from .program import ProgramForm
|
||||||
|
from .page import CommentForm, ImageForm, PageForm, ChildPageForm
|
||||||
|
from .sound import SoundForm, SoundCreateForm
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
widgets,
|
||||||
|
# ---- forms
|
||||||
|
EpisodeForm,
|
||||||
|
EpisodeSoundFormSet,
|
||||||
|
ProgramForm,
|
||||||
|
CommentForm,
|
||||||
|
ImageForm,
|
||||||
|
PageForm,
|
||||||
|
ChildPageForm,
|
||||||
|
SoundForm,
|
||||||
|
SoundCreateForm,
|
||||||
|
UserGroupFormSet,
|
||||||
|
)
|
20
aircox/forms/auth.py
Normal file
20
aircox/forms/auth.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
from django import forms
|
||||||
|
from django.forms.models import modelformset_factory
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
from aircox.forms import widgets
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ("UserGroupFormSet",)
|
||||||
|
|
||||||
|
|
||||||
|
UserGroupFormSet = modelformset_factory(
|
||||||
|
User.groups.through,
|
||||||
|
fields=("group", "user"),
|
||||||
|
widgets={
|
||||||
|
"group": forms.HiddenInput(),
|
||||||
|
"user": widgets.VueAutoComplete("api:usergroup-autocomplete", lookup="username"),
|
||||||
|
},
|
||||||
|
extra=0,
|
||||||
|
can_delete=True,
|
||||||
|
)
|
34
aircox/forms/episode.py
Normal file
34
aircox/forms/episode.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
from django import forms
|
||||||
|
from django.forms.models import modelformset_factory
|
||||||
|
|
||||||
|
from aircox import models
|
||||||
|
from .page import ChildPageForm
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ("EpisodeForm", "EpisodeSoundFormSet")
|
||||||
|
|
||||||
|
|
||||||
|
class EpisodeForm(ChildPageForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.Episode
|
||||||
|
fields = ChildPageForm.Meta.fields
|
||||||
|
|
||||||
|
|
||||||
|
EpisodeSoundFormSet = modelformset_factory(
|
||||||
|
models.EpisodeSound,
|
||||||
|
fields=(
|
||||||
|
"position",
|
||||||
|
"episode",
|
||||||
|
"sound",
|
||||||
|
"broadcast",
|
||||||
|
),
|
||||||
|
widgets={
|
||||||
|
"broadcast": forms.CheckboxInput(),
|
||||||
|
"episode": forms.HiddenInput(),
|
||||||
|
# "sound": forms.HiddenInput(),
|
||||||
|
"position": forms.HiddenInput(),
|
||||||
|
},
|
||||||
|
can_delete=True,
|
||||||
|
extra=0,
|
||||||
|
)
|
||||||
|
"""Formset used in EpisodeUpdateView."""
|
37
aircox/forms/page.py
Normal file
37
aircox/forms/page.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
|
||||||
|
from aircox import models
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ("CommentForm", "ImageForm", "PageForm", "ChildPageForm")
|
||||||
|
|
||||||
|
|
||||||
|
class CommentForm(forms.ModelForm):
|
||||||
|
nickname = forms.CharField()
|
||||||
|
email = forms.EmailField(required=False)
|
||||||
|
content = forms.CharField(widget=forms.Textarea())
|
||||||
|
|
||||||
|
nickname.widget.attrs.update({"class": "input"})
|
||||||
|
email.widget.attrs.update({"class": "input"})
|
||||||
|
content.widget.attrs.update({"class": "textarea"})
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Comment
|
||||||
|
fields = ["nickname", "email", "content"]
|
||||||
|
|
||||||
|
|
||||||
|
class ImageForm(forms.Form):
|
||||||
|
file = forms.ImageField()
|
||||||
|
|
||||||
|
|
||||||
|
class PageForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
fields = ("title", "category", "status", "cover", "content")
|
||||||
|
model = models.Page
|
||||||
|
|
||||||
|
|
||||||
|
class ChildPageForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
fields = ("title", "status", "cover", "content")
|
||||||
|
model = models.Page
|
11
aircox/forms/program.py
Normal file
11
aircox/forms/program.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
from aircox import models
|
||||||
|
from .page import PageForm
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ("ProgramForm",)
|
||||||
|
|
||||||
|
|
||||||
|
class ProgramForm(PageForm):
|
||||||
|
class Meta:
|
||||||
|
fields = PageForm.Meta.fields
|
||||||
|
model = models.Program
|
26
aircox/forms/sound.py
Normal file
26
aircox/forms/sound.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
from aircox import models
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
"SoundForm",
|
||||||
|
"SoundCreateForm",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SoundForm(forms.ModelForm):
|
||||||
|
"""SoundForm used in EpisodeUpdateView."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Sound
|
||||||
|
fields = ["name", "program", "file", "broadcast", "duration", "is_public", "is_downloadable"]
|
||||||
|
|
||||||
|
|
||||||
|
class SoundCreateForm(forms.ModelForm):
|
||||||
|
"""SoundForm used in EpisodeUpdateView."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Sound
|
||||||
|
fields = ["name", "program", "file", "broadcast", "is_public", "is_downloadable"]
|
||||||
|
widgets = {"program": forms.HiddenInput()}
|
23
aircox/forms/track.py
Normal file
23
aircox/forms/track.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
from django import forms
|
||||||
|
from django.forms.models import modelformset_factory
|
||||||
|
|
||||||
|
from aircox import models
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ("TrackFormSet",)
|
||||||
|
|
||||||
|
|
||||||
|
TrackFormSet = modelformset_factory(
|
||||||
|
models.Track,
|
||||||
|
fields=[
|
||||||
|
"position",
|
||||||
|
"episode",
|
||||||
|
"artist",
|
||||||
|
"title",
|
||||||
|
"tags",
|
||||||
|
],
|
||||||
|
widgets={"episode": forms.HiddenInput(), "position": forms.HiddenInput()},
|
||||||
|
can_delete=True,
|
||||||
|
extra=0,
|
||||||
|
)
|
||||||
|
"""Track formset used in EpisodeUpdateView."""
|
|
@ -4,14 +4,6 @@ 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"),
|
||||||
|
@ -25,10 +17,9 @@ class Migration(migrations.Migration):
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
to="auth.group",
|
to="auth.group",
|
||||||
verbose_name="editors",
|
verbose_name="editors",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
migrations.RunPython(init_groups_and_permissions),
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -7,7 +7,7 @@ from django.db import migrations, models
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("filer", "0017_image__transparent"),
|
("filer", "0017_image__transparent"),
|
||||||
("aircox", "0021_alter_schedule_timezone"),
|
("aircox", "0022_set_group_ownership"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
|
|
@ -10,7 +10,7 @@ sounds_info = {}
|
||||||
|
|
||||||
def get_sounds_info(apps, schema_editor):
|
def get_sounds_info(apps, schema_editor):
|
||||||
Sound = apps.get_model("aircox", "Sound")
|
Sound = apps.get_model("aircox", "Sound")
|
||||||
objs = Sound.objects.filter(episode__isnull=False).values(
|
objs = Sound.objects.filter().values(
|
||||||
"pk",
|
"pk",
|
||||||
"episode_id",
|
"episode_id",
|
||||||
"position",
|
"position",
|
||||||
|
@ -36,7 +36,7 @@ def restore_sounds_info(apps, schema_editor):
|
||||||
sound.broadcast = info["type"] == TYPE_ARCHIVE
|
sound.broadcast = info["type"] == TYPE_ARCHIVE
|
||||||
sound.is_removed = info["type"] == TYPE_REMOVED
|
sound.is_removed = info["type"] == TYPE_REMOVED
|
||||||
sounds.append(sound)
|
sounds.append(sound)
|
||||||
if not sound.is_removed:
|
if not sound.is_removed and info["episode_id"]:
|
||||||
obj = EpisodeSound(
|
obj = EpisodeSound(
|
||||||
sound=sound,
|
sound=sound,
|
||||||
episode_id=info["episode_id"],
|
episode_id=info["episode_id"],
|
||||||
|
|
|
@ -33,11 +33,6 @@ def _restore_for_objs(objs):
|
||||||
return updated
|
return updated
|
||||||
|
|
||||||
|
|
||||||
def set_group_ownership(*args):
|
|
||||||
for program in Program.objects.all():
|
|
||||||
program.set_group_ownership()
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("aircox", "0026_alter_sound_options_remove_sound_episode_and_more"),
|
("aircox", "0026_alter_sound_options_remove_sound_episode_and_more"),
|
||||||
|
|
|
@ -107,7 +107,10 @@ class File(models.Model):
|
||||||
|
|
||||||
def file_updated(self):
|
def file_updated(self):
|
||||||
"""Return True when file has been updated on filesystem."""
|
"""Return True when file has been updated on filesystem."""
|
||||||
return self.mtime != self.get_mtime() or self.is_removed != (not self.file_exists())
|
exists = self.file_exists()
|
||||||
|
if self.is_removed != (not exists):
|
||||||
|
return True
|
||||||
|
return exists and self.mtime != self.get_mtime()
|
||||||
|
|
||||||
def file_exists(self):
|
def file_exists(self):
|
||||||
"""Return true if the file still exists."""
|
"""Return true if the file still exists."""
|
||||||
|
@ -130,7 +133,7 @@ class File(models.Model):
|
||||||
name = name.replace("_", " ").strip()
|
name = name.replace("_", " ").strip()
|
||||||
|
|
||||||
is_removed = not self.file_exists()
|
is_removed = not self.file_exists()
|
||||||
mtime = self.get_mtime()
|
mtime = (not is_removed and self.get_mtime()) or None
|
||||||
|
|
||||||
changed = is_removed != self.is_removed or mtime != self.mtime or name != self.name
|
changed = is_removed != self.is_removed or mtime != self.mtime or name != self.name
|
||||||
self.name, self.is_removed, self.mtime = name, is_removed, mtime
|
self.name, self.is_removed, self.mtime = name, is_removed, mtime
|
||||||
|
|
|
@ -63,7 +63,7 @@ class Program(Page):
|
||||||
default=True,
|
default=True,
|
||||||
help_text=_("update later diffusions according to schedule changes"),
|
help_text=_("update later diffusions according to schedule changes"),
|
||||||
)
|
)
|
||||||
editors_group = models.ForeignKey(Group, models.CASCADE, blank=True, null=True, verbose_name=_("editors"))
|
editors_group = models.ForeignKey(Group, models.CASCADE, verbose_name=_("editors"))
|
||||||
|
|
||||||
objects = ProgramQuerySet.as_manager()
|
objects = ProgramQuerySet.as_manager()
|
||||||
detail_url_name = "program-detail"
|
detail_url_name = "program-detail"
|
||||||
|
|
|
@ -24,7 +24,7 @@ class SoundQuerySet(FileQuerySet):
|
||||||
|
|
||||||
def broadcast(self):
|
def broadcast(self):
|
||||||
"""Return sounds that are archives."""
|
"""Return sounds that are archives."""
|
||||||
return self.filter(broadcast=True)
|
return self.filter(broadcast=True, is_removed=False)
|
||||||
|
|
||||||
def playlist(self, order_by="file"):
|
def playlist(self, order_by="file"):
|
||||||
"""Return files absolute paths as a flat list (exclude sound without
|
"""Return files absolute paths as a flat list (exclude sound without
|
||||||
|
|
|
@ -7,7 +7,7 @@ from django.contrib.contenttypes.models import ContentType
|
||||||
from .models import Program
|
from .models import Program
|
||||||
|
|
||||||
|
|
||||||
__all__ = ("PagePermissions", "program_permissions")
|
__all__ = ("PagePermissions", "program")
|
||||||
|
|
||||||
|
|
||||||
class PagePermissions:
|
class PagePermissions:
|
||||||
|
@ -30,7 +30,6 @@ class PagePermissions:
|
||||||
"""Return True wether if user can edit Program or its children."""
|
"""Return True wether if user can edit Program or its children."""
|
||||||
from .models.page import ChildPage
|
from .models.page import ChildPage
|
||||||
|
|
||||||
breakpoint()
|
|
||||||
if isinstance(obj, ChildPage):
|
if isinstance(obj, ChildPage):
|
||||||
obj = obj.parent_subclass
|
obj = obj.parent_subclass
|
||||||
|
|
||||||
|
@ -43,20 +42,23 @@ class PagePermissions:
|
||||||
perm = self.perms_codename_format.format(self=self, perm=perm)
|
perm = self.perms_codename_format.format(self=self, perm=perm)
|
||||||
return user.has_perm(perm)
|
return user.has_perm(perm)
|
||||||
|
|
||||||
# TODO: bulk init
|
def init(self, obj, model=None):
|
||||||
def init(self, obj):
|
|
||||||
"""Initialize permissions for the provided obj."""
|
"""Initialize permissions for the provided obj."""
|
||||||
|
updated = False
|
||||||
created_groups = []
|
created_groups = []
|
||||||
|
|
||||||
# init groups
|
# init groups
|
||||||
for infos in self.groups:
|
for infos in self.groups:
|
||||||
group = getattr(obj, infos["field"])
|
group = getattr(obj, infos["field"])
|
||||||
|
if obj.pk == 12417:
|
||||||
|
breakpoint()
|
||||||
if not group:
|
if not group:
|
||||||
group, created = self.init_group(obj, infos)
|
group, created = self.init_group(obj, infos)
|
||||||
setattr(obj, infos["field"], group.pk)
|
setattr(obj, infos["field"], group.pk)
|
||||||
|
updated = True
|
||||||
created and created_groups.append((group, infos))
|
created and created_groups.append((group, infos))
|
||||||
|
|
||||||
if created_groups:
|
if updated:
|
||||||
obj.save()
|
obj.save()
|
||||||
|
|
||||||
# init perms
|
# init perms
|
||||||
|
@ -79,4 +81,4 @@ class PagePermissions:
|
||||||
group.permissions.add(perm)
|
group.permissions.add(perm)
|
||||||
|
|
||||||
|
|
||||||
program_permissions = PagePermissions(Program)
|
program = PagePermissions(Program)
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from . import auth
|
||||||
from .admin import TrackSerializer, UserSettingsSerializer
|
from .admin import TrackSerializer, UserSettingsSerializer
|
||||||
from .episode import EpisodeSoundSerializer, EpisodeSerializer
|
from .episode import EpisodeSoundSerializer, EpisodeSerializer
|
||||||
from .log import LogInfo, LogInfoSerializer
|
from .log import LogInfo, LogInfoSerializer
|
||||||
|
@ -5,6 +6,7 @@ from .page import CommentSerializer
|
||||||
from .sound import SoundSerializer
|
from .sound import SoundSerializer
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
"auth",
|
||||||
"CommentSerializer",
|
"CommentSerializer",
|
||||||
"LogInfo",
|
"LogInfo",
|
||||||
"LogInfoSerializer",
|
"LogInfoSerializer",
|
||||||
|
|
|
@ -10,6 +10,7 @@ Usefull context:
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
{% block head %}
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="application-name" content="aircox" />
|
<meta name="application-name" content="aircox" />
|
||||||
<meta name="description" content="{{ site.description }}" />
|
<meta name="description" content="{{ site.description }}" />
|
||||||
|
@ -27,17 +28,17 @@ Usefull context:
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
<title>
|
<title>
|
||||||
{% block head_title %}
|
{% block head-title %}
|
||||||
{% if page and page.title %}{{ page.title }} — {{ station.name }}
|
{% if page and page.title %}{{ page.title }} —
|
||||||
{% else %}{{ station.name }}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{{ station.name }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</title>
|
</title>
|
||||||
|
|
||||||
{% block head_extra %}{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body {% if request.is_mobile %}class="mobile"{% endif %}>
|
<body {% if request.is_mobile %}class="mobile"{% endif %}>
|
||||||
{% block body-head %}{% endblock %}
|
{% block body %}
|
||||||
<script id="init-script">
|
<script id="init-script">
|
||||||
window.addEventListener('load', function() {
|
window.addEventListener('load', function() {
|
||||||
{% block init-scripts %}
|
{% block init-scripts %}
|
||||||
|
@ -65,7 +66,7 @@ Usefull context:
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
{% include "./dashboard/widgets/nav.html" %}
|
{% include "./widgets/nav.html" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -115,7 +116,6 @@ Usefull context:
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</span>
|
</span>
|
||||||
{% endspaceless %}
|
{% endspaceless %}
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -157,5 +157,7 @@ Usefull context:
|
||||||
{% block player-container %}
|
{% block player-container %}
|
||||||
<div id="player">{% include "aircox/widgets/player.html" %}</div>
|
<div id="player">{% include "aircox/widgets/player.html" %}</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
{% extends "aircox/base.html" %}
|
|
||||||
{% comment %}Display detail of a BasePage{% endcomment %}
|
|
||||||
|
|
||||||
{% block head_title %}
|
|
||||||
{% block title %}{{ page.title }}{% endblock %}
|
|
||||||
—
|
|
||||||
{{ station.name }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block header %}{% if page %}{{ block.super }}{% endif %}{% endblock %}
|
|
|
@ -1,16 +1,8 @@
|
||||||
{% extends "./base.html" %}
|
{% extends "./public.html" %}
|
||||||
|
|
||||||
{% comment %}Display a list of BasePages{% endcomment %}
|
{% comment %}Display a list of BasePages{% endcomment %}
|
||||||
{% load i18n aircox %}
|
{% load i18n aircox %}
|
||||||
|
|
||||||
{% block head_title %}
|
|
||||||
{% block title %}
|
|
||||||
{{ block.super }}
|
|
||||||
{% endblock %}
|
|
||||||
—
|
|
||||||
{{ station.name }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,8 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block head-title %}
|
{% block head-title %}
|
||||||
{% block title %}{{ block.super }}{% endblock %}
|
{% block title %}
|
||||||
—
|
{% if page and page.title %}{{ page.title }} —{% endif %}
|
||||||
{{ block.super }}
|
{% endblock %}
|
||||||
|
{{ station.name }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -29,11 +29,11 @@ sound-delete-url="{% url "api:sound-detail" pk=123 %}"
|
||||||
{% for field in sound_form %}
|
{% for field in sound_form %}
|
||||||
{% with field.name as name %}
|
{% with field.name as name %}
|
||||||
{% if name in "program" %}
|
{% if name in "program" %}
|
||||||
{% include "./form_field.html" with value=field.initial field=field.field hidden=True %}
|
{% include "aircox/forms/form_field.html" with value=field.initial field=field.field hidden=True %}
|
||||||
{% elif name != "file" %}
|
{% elif name != "file" %}
|
||||||
<div class="field is-horizontal">
|
<div class="field is-horizontal">
|
||||||
<label class="label mr-3">{{ field.label }}</label>
|
<label class="label mr-3">{{ field.label }}</label>
|
||||||
{% include "./form_field.html" with value=field.initial field=field.field %}
|
{% include "aircox/forms/form_field.html" with value=field.initial field=field.field %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends "./page_form.html" %}
|
{% extends "./page_form.html" %}
|
||||||
{% load static i18n humanize honeypot aircox %}
|
{% load static i18n humanize honeypot aircox %}
|
||||||
|
|
||||||
{% block page_form %}
|
{% block page-form %}
|
||||||
<a-episode :page="{title: "{{ object.title }}", podcasts: {{ object.sounds|json }}}">
|
<a-episode :page="{title: "{{ object.title }}", podcasts: {{ object.sounds|json }}}">
|
||||||
<template v-slot="{podcasts,page}">
|
<template v-slot="{podcasts,page}">
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block content-container %}
|
{% block content %}
|
||||||
<article class="message is-danger">
|
<article class="message is-danger">
|
||||||
<div class="message-header">
|
<div class="message-header">
|
||||||
<p>{% block error_title %}{% trans "An error occurred" %}{% endblock %}</p>
|
<p>{% block error_title %}{% trans "An error occurred" %}{% endblock %}</p>
|
||||||
|
|
25
aircox/templates/aircox/forms/form_field.html
Normal file
25
aircox/templates/aircox/forms/form_field.html
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{% comment %}
|
||||||
|
Render a form field instance as field (to be used when no model instance is provided). Value is binded as vue, class to Bulma
|
||||||
|
|
||||||
|
Context:
|
||||||
|
- name: field name
|
||||||
|
- field: form field
|
||||||
|
- value: input ":value" attribute
|
||||||
|
- vbind: if True, use ":value" instead of "value"
|
||||||
|
- hidden: if True, hidden field
|
||||||
|
{% endcomment %}
|
||||||
|
{% load aircox %}
|
||||||
|
|
||||||
|
{% if field.widget.is_hidden or hidden %}
|
||||||
|
<input type="hidden" name="{{ name }}" value="{{ value|default:"" }}">
|
||||||
|
{% elif field|is_checkbox %}
|
||||||
|
<input type="checkbox" class="checkbox" name="{{ name }}" {% if value %}checked{% endif %}>
|
||||||
|
{% elif field|is_select %}
|
||||||
|
<select name="{{ name }}" class="select" value="{{ value|default:"" }}">
|
||||||
|
{% for value, label in field.widget.choices %}
|
||||||
|
<option value="{{ value }}">{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
{% else %}
|
||||||
|
<input type="text" class="input" name="{{ name }}" value="{{ value|default:"" }}">
|
||||||
|
{% endif %}
|
54
aircox/templates/aircox/forms/formset.html
Normal file
54
aircox/templates/aircox/forms/formset.html
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
{% comment %}
|
||||||
|
Base template for list editor based on formsets (tracklist_editor, playlist_editor).
|
||||||
|
|
||||||
|
Context:
|
||||||
|
- tag_id: id of parent component
|
||||||
|
- tag: vue component tag (a-playlist-editor, etc.)
|
||||||
|
- related_field: field name that target object
|
||||||
|
- object: related object
|
||||||
|
- formset: formset used to render the list editor
|
||||||
|
- formset_data: formset data
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
{% load aircox aircox_admin static i18n %}
|
||||||
|
|
||||||
|
{% with formset.form.base_fields as fields %}
|
||||||
|
{% block outer %}
|
||||||
|
<div id="{{ tag_id }}">
|
||||||
|
{{ formset.non_form_errors }}
|
||||||
|
<!-- formset.management_form -->
|
||||||
|
|
||||||
|
<{{ tag|default:"a-form-set" }}
|
||||||
|
{% block tag-attrs %}
|
||||||
|
:form-data="{{ formset_data|json }}"
|
||||||
|
:labels="window.aircox.labels"
|
||||||
|
:init-data="{% formset_inline_data formset=formset %}"
|
||||||
|
:columns="[{% for n, f in fields.items %}{% if not f.widget.is_hidden %}'{{ n }}',{% endif %}{% endfor %} ]"
|
||||||
|
settings-url="{% url "api:user-settings" %}"
|
||||||
|
data-prefix="{{ formset.prefix }}-"
|
||||||
|
{% endblock %}>
|
||||||
|
{% block inner %}
|
||||||
|
<template #rows-header-head>
|
||||||
|
{% block rows-header-head %}
|
||||||
|
<th style="max-width:2em" title="{{ fields.position.help_text }}"
|
||||||
|
aria-description="{{ fields.position.help_text }}">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fa fa-arrow-down-1-9"></i>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
{% endblock %}
|
||||||
|
</template>
|
||||||
|
{% for name, field in fields.items %}
|
||||||
|
{% if not field.widget.is_hidden and not field.is_readonly %}
|
||||||
|
<template v-slot:control-{{ name }}="{item,cell,value,attr,emit,inputName}">
|
||||||
|
{% block row-control %}
|
||||||
|
{% include "./v_form_field.html" with value="item.data."|add:name name="inputName" %}
|
||||||
|
{% endblock %}
|
||||||
|
</template>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock %}
|
||||||
|
</{{ tag }}>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
{% endwith %}
|
24
aircox/templates/aircox/forms/v_form_field.html
Normal file
24
aircox/templates/aircox/forms/v_form_field.html
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{% comment %}
|
||||||
|
Render a form field instance as field (to be used when no model instance is provided). Value is binded as vue, class to Bulma
|
||||||
|
|
||||||
|
Context:
|
||||||
|
- name: field name
|
||||||
|
- field: form field
|
||||||
|
- value: input ":v-model" attribute
|
||||||
|
- hidden: if True, hidden field
|
||||||
|
{% endcomment %}
|
||||||
|
{% load aircox %}
|
||||||
|
|
||||||
|
{% if field.widget.is_hidden or hidden %}
|
||||||
|
<input type="hidden" :name="{{ name }}" :value="{{ value|default:"" }}">
|
||||||
|
{% elif field|is_checkbox %}
|
||||||
|
<input type="checkbox" class="checkbox" :name="{{ name }}" v-model="{{ value }}">
|
||||||
|
{% elif field|is_select %}
|
||||||
|
<select :name="{{ name }}" class="select" v-model="{{ value|default:"" }}">
|
||||||
|
{% for value, label in field.widget.choices %}
|
||||||
|
<option value="{{ value }}">{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
{% else %}
|
||||||
|
<input type="text" class="input" :name="{{ name }}" v-model="{{ value|default:"" }}">
|
||||||
|
{% endif %}
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends "aircox/base.html" %}
|
{% extends "./public.html" %}
|
||||||
{% load i18n aircox %}
|
{% load i18n aircox %}
|
||||||
|
|
||||||
{% block head_title %}{{ station.name }}{% endblock %}
|
{% block head_title %}{{ station.name }}{% endblock %}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends "aircox/basepage_detail.html" %}
|
{% extends "aircox/public.html" %}
|
||||||
{% load static i18n humanize honeypot aircox %}
|
{% load static i18n humanize honeypot aircox %}
|
||||||
{% comment %}
|
{% comment %}
|
||||||
Base template used to display a Page
|
Base template used to display a Page
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends "./page_detail.html" %}
|
{% extends "./dashboard/base.html" %}
|
||||||
{% load static aircox_admin i18n %}
|
{% load static aircox_admin i18n %}
|
||||||
|
|
||||||
{% block assets %}
|
{% block assets %}
|
||||||
|
@ -20,6 +20,7 @@ aircox.labels = {% inline_labels %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block content-container %}
|
{% block content-container %}
|
||||||
<a-select-file ref="cover-select"
|
<a-select-file ref="cover-select"
|
||||||
:labels="window.aircox.labels"
|
:labels="window.aircox.labels"
|
||||||
|
@ -54,7 +55,7 @@ aircox.labels = {% inline_labels %}
|
||||||
|
|
||||||
<section class="container">
|
<section class="container">
|
||||||
<form method="post" enctype="multipart/form-data">
|
<form method="post" enctype="multipart/form-data">
|
||||||
{% block page_form %}
|
{% block page-form %}
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% for field in form %}
|
{% for field in form %}
|
||||||
{% if field.name == "cover" %}
|
{% if field.name == "cover" %}
|
||||||
|
@ -69,7 +70,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/widgets/form_field.html" with field=field.field name=field.name value=field.initial %}
|
{% include "aircox/forms/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>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends "aircox/page_detail.html" %}
|
{% extends "./page_form.html" %}
|
||||||
{% load static i18n humanize honeypot aircox %}
|
{% load static i18n humanize honeypot aircox %}
|
||||||
|
|
||||||
|
|
||||||
|
@ -6,34 +6,13 @@
|
||||||
{{ form.media }}
|
{{ form.media }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block init-scripts %}
|
{% block page-form %}
|
||||||
{% endblock %}
|
//////
|
||||||
|
{{ block.super }}
|
||||||
|
|
||||||
{% block comments %}
|
{% if editors_formset %}
|
||||||
{% endblock %}
|
<hr/>
|
||||||
|
<h2 class="title is-2">{% translate "Editors" %}</h2>
|
||||||
{% block content-container %}
|
{% include "./widgets/usergroup_formset.html" with formset=editors_formset formset_data=editors_formset_data tag_id="usergroup_formset" %}
|
||||||
<section class="container">
|
{% endif %}
|
||||||
<div>
|
|
||||||
<form method="post" enctype="multipart/form-data">{% csrf_token %}
|
|
||||||
{% csrf_token %}
|
|
||||||
{% for field in form %}
|
|
||||||
<div class="field is-horizontal">
|
|
||||||
<label class="label">{{ field.label }}</label>
|
|
||||||
<div class="control">{{ field }}</div>
|
|
||||||
</div>
|
|
||||||
{% if field.errors %}
|
|
||||||
<p class="help is-danger">{{ field.errors }}</p>
|
|
||||||
{% endif %}
|
|
||||||
{% if field.help_text %}
|
|
||||||
<p class="help">{{ field.help_text|safe }}</p>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<div class="has-text-right">
|
|
||||||
<button type="submit" class="button">{% translate "Update" %}</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
17
aircox/templates/aircox/public.html
Normal file
17
aircox/templates/aircox/public.html
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{% extends base_template|default:"./base.html" %}
|
||||||
|
|
||||||
|
|
||||||
|
{% comment %}
|
||||||
|
Override is a trick here: it allows to change title at two different different
|
||||||
|
places inside the page: inside `<title>` tag, and inside the page
|
||||||
|
content.
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
{% block head-title %}
|
||||||
|
{% block title %}
|
||||||
|
{% if page and page.title %}{{ page.title }} —{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
{{ station.name }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header %}{% if page %}{{ block.super }}{% endif %}{% endblock %}
|
4
aircox/templates/aircox/widgets/autocomplete.html
Normal file
4
aircox/templates/aircox/widgets/autocomplete.html
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<a-autocomplete
|
||||||
|
url="{{url}}"
|
||||||
|
name="{{ widget.name }}"{% if widget.value != None %} model-value="{{ widget.value|stringformat:'s' }}"{% endif %}
|
||||||
|
{% include "django/forms/widgets/attrs.html" %} />
|
|
@ -25,31 +25,12 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block actions %}
|
{% block content %}
|
||||||
{% if object.sound_set.public.count %}
|
{% if not object.content %}
|
||||||
<button class="button" @click="player.playButtonClick($event)"
|
{% with object.parent.content as content %}
|
||||||
data-sounds="{{ object.podcasts|json }}">
|
{{ block.super }}
|
||||||
<span class="icon is-small">
|
{% endwith %}
|
||||||
<span class="fas fa-play"></span>
|
{% else %}
|
||||||
</span>
|
{{ block.super }}
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block actions %}
|
|
||||||
{% has_perm page object.program.change_permission_codename simple=True as can_edit %}
|
|
||||||
{% if can_edit %}
|
|
||||||
<a class="button" href="{% url 'episode-edit' object.pk %}" target="_self">
|
|
||||||
<span class="icon is-small"><i class="fas fa-pen" alt="{% trans 'edit' %}"></i></span>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if object.sound_set.public.count %}
|
|
||||||
<button class="button" @click="player.playButtonClick($event)"
|
|
||||||
data-sounds="{{ object.podcasts|json }}">
|
|
||||||
<span class="icon is-small">
|
|
||||||
<span class="fas fa-play"></span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
10
aircox/templates/aircox/widgets/usergroup_formset.html
Normal file
10
aircox/templates/aircox/widgets/usergroup_formset.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{% extends "aircox/forms/formset.html" %}
|
||||||
|
{% load aircox %}
|
||||||
|
|
||||||
|
{% block row-control %}
|
||||||
|
{% if name == 'user' %}
|
||||||
|
{% form_field field name value %}
|
||||||
|
{% else %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
|
@ -14,6 +14,12 @@ random.seed()
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(name="form_field")
|
||||||
|
def form_field(field, name=None, value=None, **kwargs):
|
||||||
|
name = name or field.name
|
||||||
|
return field.widget.render(name=name, value=value, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name="admin_url")
|
@register.filter(name="admin_url")
|
||||||
def admin_url(obj, action):
|
def admin_url(obj, action):
|
||||||
meta = obj._meta
|
meta = obj._meta
|
||||||
|
|
|
@ -25,6 +25,7 @@ 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")
|
router.register("comment", viewsets.CommentViewSet, basename="comment")
|
||||||
|
router.register("usergroup", viewsets.UserGroupViewSet, basename="usergroup")
|
||||||
|
|
||||||
|
|
||||||
api = [
|
api = [
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic.base import TemplateView
|
from django.views.generic.base import TemplateView
|
||||||
|
|
||||||
|
@ -12,9 +12,13 @@ from .log import LogListView
|
||||||
__all__ = ("DashboardBaseView", "DashboardView", "StatisticsView")
|
__all__ = ("DashboardBaseView", "DashboardView", "StatisticsView")
|
||||||
|
|
||||||
|
|
||||||
class DashboardBaseView(LoginRequiredMixin, BaseView):
|
class DashboardBaseView(LoginRequiredMixin, UserPassesTestMixin, BaseView):
|
||||||
title = _("Dashboard")
|
title = _("Dashboard")
|
||||||
|
|
||||||
|
def test_func(self):
|
||||||
|
user = self.request.user
|
||||||
|
return user.is_staff or user.is_superuser
|
||||||
|
|
||||||
|
|
||||||
class DashboardView(DashboardBaseView, TemplateView):
|
class DashboardView(DashboardBaseView, TemplateView):
|
||||||
template_name = "aircox/dashboard/dashboard.html"
|
template_name = "aircox/dashboard/dashboard.html"
|
||||||
|
|
|
@ -52,7 +52,7 @@ class EpisodeUpdateView(UserPassesTestMixin, VueFormDataMixin, PageUpdateView):
|
||||||
|
|
||||||
def test_func(self):
|
def test_func(self):
|
||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
return permissions.program_permissions.can(self.request.user, "update", obj)
|
return permissions.program.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")
|
||||||
|
|
|
@ -194,13 +194,9 @@ class PageDetailView(BasePageDetailView):
|
||||||
|
|
||||||
class PageUpdateView(BaseView, UpdateView):
|
class PageUpdateView(BaseView, UpdateView):
|
||||||
context_object_name = "page"
|
context_object_name = "page"
|
||||||
template_name = "aircox/page_form.html"
|
|
||||||
|
|
||||||
def get_page(self):
|
def get_page(self):
|
||||||
return self.object
|
return self.object
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return self.request.path
|
return self.request.path
|
||||||
|
|
||||||
def get_comment_form(self):
|
|
||||||
return None
|
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import random
|
import random
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
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 import models, forms, permissions
|
from aircox import models, forms, permissions
|
||||||
|
from .mixins import VueFormDataMixin
|
||||||
from .page import PageDetailView, PageListView, PageUpdateView
|
from .page import PageDetailView, PageListView, PageUpdateView
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
@ -51,13 +53,39 @@ class ProgramListView(PageListView):
|
||||||
return super().get_queryset().order_by("title")
|
return super().get_queryset().order_by("title")
|
||||||
|
|
||||||
|
|
||||||
class ProgramUpdateView(UserPassesTestMixin, PageUpdateView):
|
class ProgramUpdateView(UserPassesTestMixin, VueFormDataMixin, PageUpdateView):
|
||||||
model = models.Program
|
model = models.Program
|
||||||
form_class = forms.ProgramForm
|
form_class = forms.ProgramForm
|
||||||
|
queryset = models.Program.objects.select_related("editors_group")
|
||||||
|
|
||||||
def test_func(self):
|
def test_func(self):
|
||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
return permissions.program_permissions.can(self.request.user, "update", obj)
|
return permissions.program.can(self.request.user, "update", obj)
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_editors_queryset(self, program):
|
||||||
return reverse("program-detail", kwargs={"slug": self.get_object().slug})
|
# TODO: provide username in formset initials
|
||||||
|
return User.groups.through.objects.filter(group_id=program.editors_group_id).order_by("user__username")
|
||||||
|
|
||||||
|
def get_editors_formset(self, program, **kwargs):
|
||||||
|
return forms.UserGroupFormSet(
|
||||||
|
**{
|
||||||
|
**kwargs,
|
||||||
|
"prefix": "editors",
|
||||||
|
"queryset": self.get_editors_queryset(program),
|
||||||
|
"initial": {
|
||||||
|
"group": program.editors_group_id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_context_data(self, editors_formset=None, **kwargs):
|
||||||
|
# TODO: use group and permission system
|
||||||
|
if self.request.user.is_superuser:
|
||||||
|
if editors_formset is None:
|
||||||
|
editors_formset = self.get_editors_formset(self.object)
|
||||||
|
kwargs["editors_formset_data"] = self.get_formset_data(
|
||||||
|
editors_formset, {"group": self.object.editors_group_id}
|
||||||
|
)
|
||||||
|
|
||||||
|
context = super().get_context_data(editors_formset=editors_formset, **kwargs)
|
||||||
|
return context
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from django.contrib.auth.models import User
|
||||||
from django_filters import rest_framework as drf_filters
|
from django_filters import rest_framework as drf_filters
|
||||||
from rest_framework import status, viewsets, parsers, permissions
|
from rest_framework import status, viewsets, parsers, permissions
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
|
@ -8,14 +9,37 @@ from filer.models.imagemodels import Image
|
||||||
from . import models, forms, filters, serializers
|
from . import models, forms, filters, serializers
|
||||||
from .views import BaseAPIView
|
from .views import BaseAPIView
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
"ImageViewSet",
|
"ImageViewSet",
|
||||||
"SoundViewSet",
|
"SoundViewSet",
|
||||||
"TrackROViewSet",
|
"TrackROViewSet",
|
||||||
|
"UserGroupViewSet",
|
||||||
"UserSettingsViewSet",
|
"UserSettingsViewSet",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AutocompleteMixin:
|
||||||
|
"""Based on provided filterset and serializer, add an "autocomplete" action
|
||||||
|
to the viewset.
|
||||||
|
|
||||||
|
Url ``GET`` parameters:
|
||||||
|
- `field` (many): if provided, only return provided field names
|
||||||
|
- filterset's lookups.
|
||||||
|
|
||||||
|
Return a list of values if ``field`` is provided, result of `list()` otherwise.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@action(name="autocomplete", detail=False)
|
||||||
|
def autocomplete(self, request):
|
||||||
|
field = request.GET.get("field", None)
|
||||||
|
if field:
|
||||||
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
|
values = queryset.values_list(field, flat=True).distinct()
|
||||||
|
return Response(values[:10])
|
||||||
|
return self.list(request)
|
||||||
|
|
||||||
|
|
||||||
class ImageViewSet(viewsets.ModelViewSet):
|
class ImageViewSet(viewsets.ModelViewSet):
|
||||||
parsers = (parsers.MultiPartParser,)
|
parsers = (parsers.MultiPartParser,)
|
||||||
permissions = (permissions.IsAuthenticatedOrReadOnly,)
|
permissions = (permissions.IsAuthenticatedOrReadOnly,)
|
||||||
|
@ -55,7 +79,7 @@ class SoundViewSet(BaseAPIView, viewsets.ModelViewSet):
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
|
||||||
class TrackROViewSet(viewsets.ReadOnlyModelViewSet):
|
class TrackROViewSet(AutocompleteMixin, viewsets.ReadOnlyModelViewSet):
|
||||||
"""Track viewset used for auto completion."""
|
"""Track viewset used for auto completion."""
|
||||||
|
|
||||||
serializer_class = serializers.admin.TrackSerializer
|
serializer_class = serializers.admin.TrackSerializer
|
||||||
|
@ -64,15 +88,6 @@ class TrackROViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
filterset_class = filters.TrackFilterSet
|
filterset_class = filters.TrackFilterSet
|
||||||
queryset = models.Track.objects.all()
|
queryset = models.Track.objects.all()
|
||||||
|
|
||||||
@action(name="autocomplete", detail=False)
|
|
||||||
def autocomplete(self, request):
|
|
||||||
field = request.GET.get("field", None)
|
|
||||||
if field:
|
|
||||||
queryset = self.filter_queryset(self.get_queryset())
|
|
||||||
values = queryset.values_list(field, flat=True).distinct()
|
|
||||||
return Response(values[:10])
|
|
||||||
return self.list(request)
|
|
||||||
|
|
||||||
|
|
||||||
class CommentViewSet(viewsets.ModelViewSet):
|
class CommentViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = serializers.CommentSerializer
|
serializer_class = serializers.CommentSerializer
|
||||||
|
@ -81,13 +96,19 @@ class CommentViewSet(viewsets.ModelViewSet):
|
||||||
|
|
||||||
|
|
||||||
# --- admin
|
# --- admin
|
||||||
|
class UserGroupViewSet(AutocompleteMixin, viewsets.ModelViewSet):
|
||||||
|
serializer_class = serializers.auth.UserGroupSerializer
|
||||||
|
permission_classes = (permissions.IsAdminUser,)
|
||||||
|
queryset = User.groups.through.objects.all().distinct().order_by("user__username")
|
||||||
|
|
||||||
|
|
||||||
class UserSettingsViewSet(viewsets.ViewSet):
|
class UserSettingsViewSet(viewsets.ViewSet):
|
||||||
"""User's settings specific to aircox.
|
"""User's settings specific to aircox.
|
||||||
|
|
||||||
Allow only to create and edit user's own settings.
|
Allow only to create and edit user's own settings.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
serializer_class = serializers.admin.UserSettingsSerializer
|
serializer_class = serializers.UserSettingsSerializer
|
||||||
permission_classes = (permissions.IsAuthenticated,)
|
permission_classes = (permissions.IsAuthenticated,)
|
||||||
|
|
||||||
def get_serializer(self, instance=None, **kwargs):
|
def get_serializer(self, instance=None, **kwargs):
|
||||||
|
|
|
@ -3,6 +3,7 @@ import tzlocal
|
||||||
|
|
||||||
from aircox.utils import to_seconds
|
from aircox.utils import to_seconds
|
||||||
|
|
||||||
|
from ..conf import settings
|
||||||
from .metadata import Metadata, Request
|
from .metadata import Metadata, Request
|
||||||
|
|
||||||
|
|
||||||
|
@ -76,7 +77,7 @@ class PlaylistSource(Source):
|
||||||
self.program = program
|
self.program = program
|
||||||
|
|
||||||
super().__init__(controller, id=id, **kwargs)
|
super().__init__(controller, id=id, **kwargs)
|
||||||
self.path = os.path.join(self.station.path, f"{self.id}.m3u")
|
self.path = settings.get_dir(self.station, f"{self.id}.m3u")
|
||||||
|
|
||||||
def get_sound_queryset(self):
|
def get_sound_queryset(self):
|
||||||
"""Get playlist's sounds queryset."""
|
"""Get playlist's sounds queryset."""
|
||||||
|
@ -88,6 +89,7 @@ class PlaylistSource(Source):
|
||||||
|
|
||||||
def write_playlist(self, playlist=[]):
|
def write_playlist(self, playlist=[]):
|
||||||
"""Write playlist to file."""
|
"""Write playlist to file."""
|
||||||
|
playlist = playlist or self.get_playlist()
|
||||||
os.makedirs(os.path.dirname(self.path), exist_ok=True)
|
os.makedirs(os.path.dirname(self.path), exist_ok=True)
|
||||||
with open(self.path, "w") as file:
|
with open(self.path, "w") as file:
|
||||||
file.write("\n".join(playlist or []))
|
file.write("\n".join(playlist or []))
|
||||||
|
|
|
@ -95,6 +95,8 @@ class Streamer:
|
||||||
data = render_to_string(
|
data = render_to_string(
|
||||||
self.template_name,
|
self.template_name,
|
||||||
{
|
{
|
||||||
|
"dir": settings.get_dir(self.station),
|
||||||
|
"log_file": settings.get_dir(self.station, "liquidsoap.log"),
|
||||||
"station": self.station,
|
"station": self.station,
|
||||||
"streamer": self,
|
"streamer": self,
|
||||||
},
|
},
|
||||||
|
|
|
@ -53,13 +53,13 @@ class SourceSerializer(MetadataSerializer):
|
||||||
class PlaylistSerializer(SourceSerializer):
|
class PlaylistSerializer(SourceSerializer):
|
||||||
program = serializers.CharField(source="program.id")
|
program = serializers.CharField(source="program.id")
|
||||||
|
|
||||||
url_name = "admin:api:streamer-playlist-detail"
|
url_name = "streamer:api:streamer-playlist-detail"
|
||||||
|
|
||||||
|
|
||||||
class QueueSourceSerializer(SourceSerializer):
|
class QueueSourceSerializer(SourceSerializer):
|
||||||
queue = serializers.ListField(child=RequestSerializer(), source="requests")
|
queue = serializers.ListField(child=RequestSerializer(), source="requests")
|
||||||
|
|
||||||
url_name = "admin:api:streamer-queue-detail"
|
url_name = "streamer:api:streamer-queue-detail"
|
||||||
|
|
||||||
|
|
||||||
class StreamerSerializer(BaseSerializer):
|
class StreamerSerializer(BaseSerializer):
|
||||||
|
@ -69,7 +69,7 @@ class StreamerSerializer(BaseSerializer):
|
||||||
playlists = serializers.ListField(child=PlaylistSerializer())
|
playlists = serializers.ListField(child=PlaylistSerializer())
|
||||||
queues = serializers.ListField(child=QueueSourceSerializer())
|
queues = serializers.ListField(child=QueueSourceSerializer())
|
||||||
|
|
||||||
url_name = "admin:api:streamer-detail"
|
url_name = "streamer:api:streamer-detail"
|
||||||
|
|
||||||
def get_url(self, obj, **kwargs):
|
def get_url(self, obj, **kwargs):
|
||||||
kwargs["pk"] = obj.station.pk
|
kwargs["pk"] = obj.station.pk
|
||||||
|
|
|
@ -80,7 +80,7 @@ end
|
||||||
{% block config %}
|
{% block config %}
|
||||||
set("server.socket", true)
|
set("server.socket", true)
|
||||||
set("server.socket.path", "{{ streamer.socket_path }}")
|
set("server.socket.path", "{{ streamer.socket_path }}")
|
||||||
set("log.file.path", "{{ station.path }}/liquidsoap.log")
|
set("log.file.path", "{{ log_file }}")
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block config_extras %}
|
{% block config_extras %}
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
{% extends "admin/base_site.html" %}
|
{% extends "aircox/dashboard/base.html" %}
|
||||||
{% comment %}Admin tools used to manage the streamer.{% endcomment %}
|
|
||||||
{% load i18n static %}
|
{% load i18n static %}
|
||||||
|
|
||||||
{% block init-scripts %}
|
|
||||||
aircox.init({apiUrl: "{% url "admin:api:streamer-list" %}"},
|
|
||||||
{config: window.StreamerApp})
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block title %}{% translate "Streamer monitor" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content-container %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
<div id="app">
|
<div class="container">
|
||||||
<a-streamer api-url="{% url "admin:api:streamer-list" %}">
|
<a-streamer api-url="{% url "streamer:api:streamer-list" %}">
|
||||||
<template v-slot="{streamer,streamers,sources,fetchStreamers,Sound}">
|
<template v-slot="{streamer,streamers,sources,fetchStreamers,Sound}">
|
||||||
<div class="navbar toolbar">
|
<div class="navbar toolbar">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
|
|
|
@ -1,33 +1,25 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.urls import include, path
|
||||||
|
|
||||||
from aircox.viewsets import SoundViewSet
|
from aircox.viewsets import SoundViewSet
|
||||||
|
|
||||||
from . import viewsets
|
from . import views, viewsets
|
||||||
from .views import StreamerAdminView
|
|
||||||
|
|
||||||
admin.site.route_view(
|
|
||||||
"tools/streamer",
|
|
||||||
StreamerAdminView.as_view(),
|
|
||||||
"tools-streamer",
|
|
||||||
label=_("Streamer Monitor"),
|
|
||||||
)
|
|
||||||
|
|
||||||
streamer_prefix = "streamer/(?P<station_pk>[0-9]+)/"
|
__all__ = ("api", "urls")
|
||||||
|
|
||||||
|
|
||||||
|
prefix = "(?P<station_pk>[0-9]+)/"
|
||||||
|
|
||||||
|
|
||||||
router = admin.site.router
|
router = admin.site.router
|
||||||
router.register(
|
router.register(prefix + "playlist", viewsets.PlaylistSourceViewSet, basename="streamer-playlist")
|
||||||
streamer_prefix + "playlist",
|
router.register(prefix + "queue", viewsets.QueueSourceViewSet, basename="streamer-queue")
|
||||||
viewsets.PlaylistSourceViewSet,
|
|
||||||
basename="streamer-playlist",
|
|
||||||
)
|
|
||||||
router.register(
|
|
||||||
streamer_prefix + "queue",
|
|
||||||
viewsets.QueueSourceViewSet,
|
|
||||||
basename="streamer-queue",
|
|
||||||
)
|
|
||||||
router.register("streamer", viewsets.StreamerViewSet, basename="streamer")
|
router.register("streamer", viewsets.StreamerViewSet, basename="streamer")
|
||||||
router.register("sound", SoundViewSet, basename="sound")
|
router.register("sound", SoundViewSet, basename="sound")
|
||||||
|
|
||||||
urls = []
|
api = router.urls
|
||||||
|
urls = [
|
||||||
|
path("api/", include((api, "aircox_streamer"), namespace="api")),
|
||||||
|
path("", views.StreamerView.as_view(), name="dashboard-streamer"),
|
||||||
|
]
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
|
|
||||||
from aircox.views.admin import AdminMixin
|
from aircox.views.dashboard import DashboardBaseView
|
||||||
from .controllers import streamers
|
from .controllers import streamers
|
||||||
|
|
||||||
|
|
||||||
class StreamerAdminView(AdminMixin, TemplateView):
|
class StreamerView(DashboardBaseView, TemplateView):
|
||||||
template_name = "aircox_streamer/streamer.html"
|
template_name = "aircox_streamer/streamer.html"
|
||||||
title = _("Streamer Monitor")
|
title = _("Streamer")
|
||||||
streamers = streamers
|
streamers = streamers
|
||||||
|
|
||||||
def dispatch(self, *args, **kwargs):
|
def dispatch(self, *args, **kwargs):
|
||||||
|
|
|
@ -43,6 +43,8 @@ class ControllerViewSet(viewsets.ViewSet):
|
||||||
if station_pk is None:
|
if station_pk is None:
|
||||||
station_pk = self.request.station.pk
|
station_pk = self.request.station.pk
|
||||||
self.streamers.fetch()
|
self.streamers.fetch()
|
||||||
|
if station_pk is None:
|
||||||
|
return None
|
||||||
if station_pk not in self.streamers:
|
if station_pk not in self.streamers:
|
||||||
raise Http404("station not found")
|
raise Http404("station not found")
|
||||||
return self.streamers[station_pk]
|
return self.streamers[station_pk]
|
||||||
|
@ -78,7 +80,7 @@ class StreamerViewSet(ControllerViewSet):
|
||||||
def dispatch(self, request, *args, pk=None, **kwargs):
|
def dispatch(self, request, *args, pk=None, **kwargs):
|
||||||
if pk is not None:
|
if pk is not None:
|
||||||
kwargs.setdefault("station_pk", pk)
|
kwargs.setdefault("station_pk", pk)
|
||||||
self.streamer = self.get_streamer(request, **kwargs)
|
self.streamer = self.get_streamer(**kwargs)
|
||||||
self.object = self.streamer
|
self.object = self.streamer
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,8 @@
|
||||||
"eslint-plugin-vue": "^8.0.3",
|
"eslint-plugin-vue": "^8.0.3",
|
||||||
"sass": "^1.49.9",
|
"sass": "^1.49.9",
|
||||||
"sass-loader": "^12.6.0",
|
"sass-loader": "^12.6.0",
|
||||||
"vue-cli": "^2.9.6"
|
"vue-cli": "^2.9.6",
|
||||||
|
"webpack-cli": "^5.1.4"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"root": true,
|
"root": true,
|
||||||
|
|
|
@ -232,6 +232,8 @@ export default {
|
||||||
: fetch(url, Model.getOptions()).then(d => d.json())
|
: fetch(url, Model.getOptions()).then(d => d.json())
|
||||||
|
|
||||||
promise = promise.then(items => {
|
promise = promise.then(items => {
|
||||||
|
if(items.results)
|
||||||
|
items = items.results
|
||||||
this.items = items.filter((i) => i) || []
|
this.items = items.filter((i) => i) || []
|
||||||
this.promise = null;
|
this.promise = null;
|
||||||
this.move(0)
|
this.move(0)
|
||||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -184,9 +184,9 @@ THUMBNAIL_PROCESSORS = (
|
||||||
# Enabled applications
|
# Enabled applications
|
||||||
INSTALLED_APPS = (
|
INSTALLED_APPS = (
|
||||||
"radiocampus",
|
"radiocampus",
|
||||||
|
"aircox_streamer.apps.AircoxStreamerConfig",
|
||||||
"aircox.apps.AircoxConfig",
|
"aircox.apps.AircoxConfig",
|
||||||
"aircox.apps.AircoxAdminConfig",
|
"aircox.apps.AircoxAdminConfig",
|
||||||
"aircox_streamer.apps.AircoxStreamerConfig",
|
|
||||||
# Aircox dependencies
|
# Aircox dependencies
|
||||||
"rest_framework",
|
"rest_framework",
|
||||||
"django_filters",
|
"django_filters",
|
||||||
|
|
|
@ -22,16 +22,14 @@ from django.urls import include, path
|
||||||
import aircox.urls
|
import aircox.urls
|
||||||
import aircox_streamer.urls
|
import aircox_streamer.urls
|
||||||
|
|
||||||
urlpatterns = (
|
urlpatterns = [
|
||||||
aircox.urls.urls
|
*aircox.urls.urls,
|
||||||
+ aircox_streamer.urls.urls
|
path("streamer/", include((aircox_streamer.urls.urls, "aircox_streamer"), namespace="streamer")),
|
||||||
+ [
|
path("admin/", admin.site.urls),
|
||||||
path("admin/", admin.site.urls),
|
path("accounts/", include("django.contrib.auth.urls")),
|
||||||
path("accounts/", include("django.contrib.auth.urls")),
|
path("ckeditor/", include("ckeditor_uploader.urls")),
|
||||||
path("ckeditor/", include("ckeditor_uploader.urls")),
|
path("filer/", include("filer.urls")),
|
||||||
path("filer/", include("filer.urls")),
|
]
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + static(
|
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + static(
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends "aircox/base.html" %}
|
{% extends "aircox/base.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block head_extra %}
|
{% block assets %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
Django~=4.1
|
Django~=5.0
|
||||||
djangorestframework~=3.13
|
djangorestframework~=3.14
|
||||||
django-model-utils>=4.2
|
django-model-utils>=4.3
|
||||||
django-filter~=22.1
|
django-filter~=22.1
|
||||||
|
|
||||||
django-content-editor~=6.3
|
django-content-editor~=6.3
|
||||||
django-filer~=2.2
|
django-filer~=3.1
|
||||||
django-honeypot~=1.0
|
django-honeypot~=1.0
|
||||||
django-taggit~=3.0
|
django-taggit~=3.0
|
||||||
django-admin-sortable2~=2.1
|
django-admin-sortable2~=2.1
|
||||||
|
|
Loading…
Reference in New Issue
Block a user