rm file
This commit is contained in:
		@ -233,12 +233,12 @@ class SoundMonitor:
 | 
			
		||||
        if not program.ensure_dir(subdir):
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        subdir = os.path.join(program.abspath, subdir)
 | 
			
		||||
        abs_subdir = os.path.join(program.abspath, subdir)
 | 
			
		||||
        sounds = []
 | 
			
		||||
 | 
			
		||||
        # sounds in directory
 | 
			
		||||
        for path in os.listdir(subdir):
 | 
			
		||||
            path = os.path.join(subdir, path)
 | 
			
		||||
        for path in os.listdir(abs_subdir):
 | 
			
		||||
            path = os.path.join(abs_subdir, path)
 | 
			
		||||
            if not path.endswith(settings.SOUND_FILE_EXT):
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
@ -247,7 +247,7 @@ class SoundMonitor:
 | 
			
		||||
            sounds.append(sound_file.sound.pk)
 | 
			
		||||
 | 
			
		||||
        # 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)
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def init_groups_and_permissions(app, schema_editor):
 | 
			
		||||
    from aircox.permissions import program_permissions
 | 
			
		||||
 | 
			
		||||
    Program = app.get_model("aircox", "Program")
 | 
			
		||||
    for program in Program.objects.all():
 | 
			
		||||
        program_permissions.init(program)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("auth", "0012_alter_user_first_name_max_length"),
 | 
			
		||||
@ -25,10 +17,9 @@ class Migration(migrations.Migration):
 | 
			
		||||
            field=models.ForeignKey(
 | 
			
		||||
                blank=True,
 | 
			
		||||
                null=True,
 | 
			
		||||
                on_delete=django.db.models.deletion.CASCADE,
 | 
			
		||||
                on_delete=django.db.models.deletion.SET_NULL,
 | 
			
		||||
                to="auth.group",
 | 
			
		||||
                verbose_name="editors",
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RunPython(init_groups_and_permissions),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,7 @@ from django.db import migrations, models
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("filer", "0017_image__transparent"),
 | 
			
		||||
        ("aircox", "0021_alter_schedule_timezone"),
 | 
			
		||||
        ("aircox", "0022_set_group_ownership"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@ sounds_info = {}
 | 
			
		||||
 | 
			
		||||
def get_sounds_info(apps, schema_editor):
 | 
			
		||||
    Sound = apps.get_model("aircox", "Sound")
 | 
			
		||||
    objs = Sound.objects.filter(episode__isnull=False).values(
 | 
			
		||||
    objs = Sound.objects.filter().values(
 | 
			
		||||
        "pk",
 | 
			
		||||
        "episode_id",
 | 
			
		||||
        "position",
 | 
			
		||||
@ -36,7 +36,7 @@ def restore_sounds_info(apps, schema_editor):
 | 
			
		||||
            sound.broadcast = info["type"] == TYPE_ARCHIVE
 | 
			
		||||
            sound.is_removed = info["type"] == TYPE_REMOVED
 | 
			
		||||
            sounds.append(sound)
 | 
			
		||||
            if not sound.is_removed:
 | 
			
		||||
            if not sound.is_removed and info["episode_id"]:
 | 
			
		||||
                obj = EpisodeSound(
 | 
			
		||||
                    sound=sound,
 | 
			
		||||
                    episode_id=info["episode_id"],
 | 
			
		||||
 | 
			
		||||
@ -33,11 +33,6 @@ def _restore_for_objs(objs):
 | 
			
		||||
    return updated
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def set_group_ownership(*args):
 | 
			
		||||
    for program in Program.objects.all():
 | 
			
		||||
        program.set_group_ownership()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("aircox", "0026_alter_sound_options_remove_sound_episode_and_more"),
 | 
			
		||||
 | 
			
		||||
@ -107,7 +107,10 @@ class File(models.Model):
 | 
			
		||||
 | 
			
		||||
    def file_updated(self):
 | 
			
		||||
        """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):
 | 
			
		||||
        """Return true if the file still exists."""
 | 
			
		||||
@ -130,7 +133,7 @@ class File(models.Model):
 | 
			
		||||
            name = name.replace("_", " ").strip()
 | 
			
		||||
 | 
			
		||||
        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
 | 
			
		||||
        self.name, self.is_removed, self.mtime = name, is_removed, mtime
 | 
			
		||||
 | 
			
		||||
@ -63,7 +63,7 @@ class Program(Page):
 | 
			
		||||
        default=True,
 | 
			
		||||
        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()
 | 
			
		||||
    detail_url_name = "program-detail"
 | 
			
		||||
 | 
			
		||||
@ -24,7 +24,7 @@ class SoundQuerySet(FileQuerySet):
 | 
			
		||||
 | 
			
		||||
    def broadcast(self):
 | 
			
		||||
        """Return sounds that are archives."""
 | 
			
		||||
        return self.filter(broadcast=True)
 | 
			
		||||
        return self.filter(broadcast=True, is_removed=False)
 | 
			
		||||
 | 
			
		||||
    def playlist(self, order_by="file"):
 | 
			
		||||
        """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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = ("PagePermissions", "program_permissions")
 | 
			
		||||
__all__ = ("PagePermissions", "program")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PagePermissions:
 | 
			
		||||
@ -30,7 +30,6 @@ class PagePermissions:
 | 
			
		||||
        """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
 | 
			
		||||
 | 
			
		||||
@ -43,20 +42,23 @@ class PagePermissions:
 | 
			
		||||
        perm = self.perms_codename_format.format(self=self, perm=perm)
 | 
			
		||||
        return user.has_perm(perm)
 | 
			
		||||
 | 
			
		||||
    # TODO: bulk init
 | 
			
		||||
    def init(self, obj):
 | 
			
		||||
    def init(self, obj, model=None):
 | 
			
		||||
        """Initialize permissions for the provided obj."""
 | 
			
		||||
        updated = False
 | 
			
		||||
        created_groups = []
 | 
			
		||||
 | 
			
		||||
        # init groups
 | 
			
		||||
        for infos in self.groups:
 | 
			
		||||
            group = getattr(obj, infos["field"])
 | 
			
		||||
            if obj.pk == 12417:
 | 
			
		||||
                breakpoint()
 | 
			
		||||
            if not group:
 | 
			
		||||
                group, created = self.init_group(obj, infos)
 | 
			
		||||
                setattr(obj, infos["field"], group.pk)
 | 
			
		||||
                updated = True
 | 
			
		||||
                created and created_groups.append((group, infos))
 | 
			
		||||
 | 
			
		||||
        if created_groups:
 | 
			
		||||
        if updated:
 | 
			
		||||
            obj.save()
 | 
			
		||||
 | 
			
		||||
        # init perms
 | 
			
		||||
@ -79,4 +81,4 @@ class PagePermissions:
 | 
			
		||||
                group.permissions.add(perm)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
program_permissions = PagePermissions(Program)
 | 
			
		||||
program = PagePermissions(Program)
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
from . import auth
 | 
			
		||||
from .admin import TrackSerializer, UserSettingsSerializer
 | 
			
		||||
from .episode import EpisodeSoundSerializer, EpisodeSerializer
 | 
			
		||||
from .log import LogInfo, LogInfoSerializer
 | 
			
		||||
@ -5,6 +6,7 @@ from .page import CommentSerializer
 | 
			
		||||
from .sound import SoundSerializer
 | 
			
		||||
 | 
			
		||||
__all__ = (
 | 
			
		||||
    "auth",
 | 
			
		||||
    "CommentSerializer",
 | 
			
		||||
    "LogInfo",
 | 
			
		||||
    "LogInfoSerializer",
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,7 @@ Usefull context:
 | 
			
		||||
{% endcomment %}
 | 
			
		||||
<html>
 | 
			
		||||
    <head>
 | 
			
		||||
    {% block head %}
 | 
			
		||||
        <meta charset="utf-8" />
 | 
			
		||||
        <meta name="application-name" content="aircox" />
 | 
			
		||||
        <meta name="description" content="{{ site.description }}" />
 | 
			
		||||
@ -27,17 +28,17 @@ Usefull context:
 | 
			
		||||
        {% endblock %}
 | 
			
		||||
 | 
			
		||||
        <title>
 | 
			
		||||
            {% block head_title %}
 | 
			
		||||
            {% if page and page.title %}{{ page.title }} — {{ station.name }}
 | 
			
		||||
            {% else %}{{ station.name }}
 | 
			
		||||
            {% block head-title %}
 | 
			
		||||
            {% if page and page.title %}{{ page.title }} —
 | 
			
		||||
            {% endif %}
 | 
			
		||||
            {{ station.name }}
 | 
			
		||||
            {% endblock %}
 | 
			
		||||
        </title>
 | 
			
		||||
 | 
			
		||||
        {% block head_extra %}{% endblock %}
 | 
			
		||||
    {% endblock %}
 | 
			
		||||
    </head>
 | 
			
		||||
    <body {% if request.is_mobile %}class="mobile"{% endif %}>
 | 
			
		||||
        {% block body-head %}{% endblock %}
 | 
			
		||||
    {% block body %}
 | 
			
		||||
        <script id="init-script">
 | 
			
		||||
            window.addEventListener('load', function() {
 | 
			
		||||
                {% block init-scripts %}
 | 
			
		||||
@ -65,7 +66,7 @@ Usefull context:
 | 
			
		||||
                        {% endfor %}
 | 
			
		||||
                        {% endblock %}
 | 
			
		||||
                        {% if user.is_authenticated %}
 | 
			
		||||
                        {% include "./dashboard/widgets/nav.html" %}
 | 
			
		||||
                        {% include "./widgets/nav.html" %}
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% endblock %}
 | 
			
		||||
@ -115,7 +116,6 @@ Usefull context:
 | 
			
		||||
                        {% endblock %}
 | 
			
		||||
                        </span>
 | 
			
		||||
                        {% endspaceless %}
 | 
			
		||||
                        </span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% endblock %}
 | 
			
		||||
                    </div>
 | 
			
		||||
@ -157,5 +157,7 @@ Usefull context:
 | 
			
		||||
        {% block player-container %}
 | 
			
		||||
        <div id="player">{% include "aircox/widgets/player.html" %}</div>
 | 
			
		||||
        {% endblock %}
 | 
			
		||||
 | 
			
		||||
    {% endblock %}
 | 
			
		||||
    </body>
 | 
			
		||||
</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 %}
 | 
			
		||||
{% load i18n aircox %}
 | 
			
		||||
 | 
			
		||||
{% block head_title %}
 | 
			
		||||
    {% block title %}
 | 
			
		||||
        {{ block.super }}
 | 
			
		||||
    {% endblock %}
 | 
			
		||||
    —
 | 
			
		||||
    {{ station.name }}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block main %}
 | 
			
		||||
{{ block.super }}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,8 @@
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block head-title %}
 | 
			
		||||
    {% block title %}{{ block.super }}{% endblock %}
 | 
			
		||||
    —
 | 
			
		||||
    {{ block.super }}
 | 
			
		||||
    {% block title %}
 | 
			
		||||
    {% if page and page.title %}{{ page.title }} —{% endif %}
 | 
			
		||||
    {% endblock %}
 | 
			
		||||
    {{ station.name }}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
@ -29,11 +29,11 @@ sound-delete-url="{% url "api:sound-detail" pk=123 %}"
 | 
			
		||||
    {% for field in sound_form %}
 | 
			
		||||
    {% with field.name as name %}
 | 
			
		||||
    {% 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" %}
 | 
			
		||||
    <div class="field is-horizontal">
 | 
			
		||||
        <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>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
    {% endwith %}
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
{% extends "./page_form.html" %}
 | 
			
		||||
{% load static i18n humanize honeypot aircox %}
 | 
			
		||||
 | 
			
		||||
{% block page_form %}
 | 
			
		||||
{% block page-form %}
 | 
			
		||||
<a-episode :page="{title: "{{ object.title }}", podcasts: {{ object.sounds|json }}}">
 | 
			
		||||
    <template v-slot="{podcasts,page}">
 | 
			
		||||
        {{ block.super }}
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,7 @@
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{% block content-container %}
 | 
			
		||||
{% block content %}
 | 
			
		||||
<article class="message is-danger">
 | 
			
		||||
    <div class="message-header">
 | 
			
		||||
        <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 %}
 | 
			
		||||
 | 
			
		||||
{% 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 %}
 | 
			
		||||
{% comment %}
 | 
			
		||||
Base template used to display a Page
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
{% extends "./page_detail.html" %}
 | 
			
		||||
{% extends "./dashboard/base.html" %}
 | 
			
		||||
{% load static aircox_admin i18n %}
 | 
			
		||||
 | 
			
		||||
{% block assets %}
 | 
			
		||||
@ -20,6 +20,7 @@ aircox.labels = {% inline_labels %}
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{% block content-container %}
 | 
			
		||||
<a-select-file ref="cover-select"
 | 
			
		||||
    :labels="window.aircox.labels"
 | 
			
		||||
@ -54,7 +55,7 @@ aircox.labels = {% inline_labels %}
 | 
			
		||||
 | 
			
		||||
<section class="container">
 | 
			
		||||
<form method="post" enctype="multipart/form-data">
 | 
			
		||||
    {% block page_form %}
 | 
			
		||||
    {% block page-form %}
 | 
			
		||||
    {% csrf_token %}
 | 
			
		||||
    {% for field in form %}
 | 
			
		||||
    {% if field.name == "cover" %}
 | 
			
		||||
@ -69,7 +70,7 @@ aircox.labels = {% inline_labels %}
 | 
			
		||||
            {% elif field.name == "content" %}
 | 
			
		||||
            <textarea name="{{ field.name }}" class="is-fullwidth height-25">{{ field.value|striptags|safe }}</textarea>
 | 
			
		||||
            {% else %}
 | 
			
		||||
            {% include "./dashboard/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 %}
 | 
			
		||||
        </div>
 | 
			
		||||
        <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 %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -6,34 +6,13 @@
 | 
			
		||||
  {{ form.media }}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block init-scripts %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
{% block page-form %}
 | 
			
		||||
//////
 | 
			
		||||
{{ block.super }}
 | 
			
		||||
 | 
			
		||||
{% block comments %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content-container %}
 | 
			
		||||
<section class="container">
 | 
			
		||||
<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>
 | 
			
		||||
{% if editors_formset %}
 | 
			
		||||
<hr/>
 | 
			
		||||
<h2 class="title is-2">{% translate "Editors" %}</h2>
 | 
			
		||||
{% include "./widgets/usergroup_formset.html" with formset=editors_formset formset_data=editors_formset_data tag_id="usergroup_formset" %}
 | 
			
		||||
{% 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 %}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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 %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{% block actions %}
 | 
			
		||||
{% if object.sound_set.public.count %}
 | 
			
		||||
<button class="button" @click="player.playButtonClick($event)"
 | 
			
		||||
        data-sounds="{{ object.podcasts|json }}">
 | 
			
		||||
    <span class="icon is-small">
 | 
			
		||||
        <span class="fas fa-play"></span>
 | 
			
		||||
    </span>
 | 
			
		||||
</button>
 | 
			
		||||
{% endif %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block actions %}
 | 
			
		||||
{% has_perm page object.program.change_permission_codename simple=True as can_edit %}
 | 
			
		||||
{% if can_edit %}
 | 
			
		||||
  <a class="button" href="{% url 'episode-edit' object.pk %}" target="_self">
 | 
			
		||||
    <span class="icon is-small"><i class="fas fa-pen" alt="{% trans 'edit' %}"></i></span>
 | 
			
		||||
  </a>
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
{% if object.sound_set.public.count %}
 | 
			
		||||
<button class="button" @click="player.playButtonClick($event)"
 | 
			
		||||
        data-sounds="{{ object.podcasts|json }}">
 | 
			
		||||
    <span class="icon is-small">
 | 
			
		||||
        <span class="fas fa-play"></span>
 | 
			
		||||
    </span>
 | 
			
		||||
</button>
 | 
			
		||||
{% block content %}
 | 
			
		||||
{% if not object.content %}
 | 
			
		||||
    {% with object.parent.content as content %}
 | 
			
		||||
    {{ block.super }}
 | 
			
		||||
    {% endwith %}
 | 
			
		||||
{% else %}
 | 
			
		||||
    {{ block.super }}
 | 
			
		||||
{% endif %}
 | 
			
		||||
{% 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.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")
 | 
			
		||||
def admin_url(obj, action):
 | 
			
		||||
    meta = obj._meta
 | 
			
		||||
 | 
			
		||||
@ -25,6 +25,7 @@ router.register("images", viewsets.ImageViewSet, basename="image")
 | 
			
		||||
router.register("sound", viewsets.SoundViewSet, basename="sound")
 | 
			
		||||
router.register("track", viewsets.TrackROViewSet, basename="track")
 | 
			
		||||
router.register("comment", viewsets.CommentViewSet, basename="comment")
 | 
			
		||||
router.register("usergroup", viewsets.UserGroupViewSet, basename="usergroup")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
api = [
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
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.views.generic.base import TemplateView
 | 
			
		||||
 | 
			
		||||
@ -12,9 +12,13 @@ from .log import LogListView
 | 
			
		||||
__all__ = ("DashboardBaseView", "DashboardView", "StatisticsView")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DashboardBaseView(LoginRequiredMixin, BaseView):
 | 
			
		||||
class DashboardBaseView(LoginRequiredMixin, UserPassesTestMixin, BaseView):
 | 
			
		||||
    title = _("Dashboard")
 | 
			
		||||
 | 
			
		||||
    def test_func(self):
 | 
			
		||||
        user = self.request.user
 | 
			
		||||
        return user.is_staff or user.is_superuser
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DashboardView(DashboardBaseView, TemplateView):
 | 
			
		||||
    template_name = "aircox/dashboard/dashboard.html"
 | 
			
		||||
 | 
			
		||||
@ -52,7 +52,7 @@ class EpisodeUpdateView(UserPassesTestMixin, VueFormDataMixin, PageUpdateView):
 | 
			
		||||
 | 
			
		||||
    def test_func(self):
 | 
			
		||||
        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):
 | 
			
		||||
        return Track.objects.filter(episode=episode).order_by("position")
 | 
			
		||||
 | 
			
		||||
@ -194,13 +194,9 @@ class PageDetailView(BasePageDetailView):
 | 
			
		||||
 | 
			
		||||
class PageUpdateView(BaseView, UpdateView):
 | 
			
		||||
    context_object_name = "page"
 | 
			
		||||
    template_name = "aircox/page_form.html"
 | 
			
		||||
 | 
			
		||||
    def get_page(self):
 | 
			
		||||
        return self.object
 | 
			
		||||
 | 
			
		||||
    def get_success_url(self):
 | 
			
		||||
        return self.request.path
 | 
			
		||||
 | 
			
		||||
    def get_comment_form(self):
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,11 @@
 | 
			
		||||
import random
 | 
			
		||||
 | 
			
		||||
from django.contrib.auth.models import User
 | 
			
		||||
from django.contrib.auth.mixins import UserPassesTestMixin
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
 | 
			
		||||
from aircox import models, forms, permissions
 | 
			
		||||
from .mixins import VueFormDataMixin
 | 
			
		||||
from .page import PageDetailView, PageListView, PageUpdateView
 | 
			
		||||
 | 
			
		||||
__all__ = (
 | 
			
		||||
@ -51,13 +53,39 @@ class ProgramListView(PageListView):
 | 
			
		||||
        return super().get_queryset().order_by("title")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ProgramUpdateView(UserPassesTestMixin, PageUpdateView):
 | 
			
		||||
class ProgramUpdateView(UserPassesTestMixin, VueFormDataMixin, PageUpdateView):
 | 
			
		||||
    model = models.Program
 | 
			
		||||
    form_class = forms.ProgramForm
 | 
			
		||||
    queryset = models.Program.objects.select_related("editors_group")
 | 
			
		||||
 | 
			
		||||
    def test_func(self):
 | 
			
		||||
        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):
 | 
			
		||||
        return reverse("program-detail", kwargs={"slug": self.get_object().slug})
 | 
			
		||||
    def get_editors_queryset(self, program):
 | 
			
		||||
        # 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 rest_framework import status, viewsets, parsers, permissions
 | 
			
		||||
from rest_framework.decorators import action
 | 
			
		||||
@ -8,14 +9,37 @@ from filer.models.imagemodels import Image
 | 
			
		||||
from . import models, forms, filters, serializers
 | 
			
		||||
from .views import BaseAPIView
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = (
 | 
			
		||||
    "ImageViewSet",
 | 
			
		||||
    "SoundViewSet",
 | 
			
		||||
    "TrackROViewSet",
 | 
			
		||||
    "UserGroupViewSet",
 | 
			
		||||
    "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):
 | 
			
		||||
    parsers = (parsers.MultiPartParser,)
 | 
			
		||||
    permissions = (permissions.IsAuthenticatedOrReadOnly,)
 | 
			
		||||
@ -55,7 +79,7 @@ class SoundViewSet(BaseAPIView, viewsets.ModelViewSet):
 | 
			
		||||
        return query
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TrackROViewSet(viewsets.ReadOnlyModelViewSet):
 | 
			
		||||
class TrackROViewSet(AutocompleteMixin, viewsets.ReadOnlyModelViewSet):
 | 
			
		||||
    """Track viewset used for auto completion."""
 | 
			
		||||
 | 
			
		||||
    serializer_class = serializers.admin.TrackSerializer
 | 
			
		||||
@ -64,15 +88,6 @@ class TrackROViewSet(viewsets.ReadOnlyModelViewSet):
 | 
			
		||||
    filterset_class = filters.TrackFilterSet
 | 
			
		||||
    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):
 | 
			
		||||
    serializer_class = serializers.CommentSerializer
 | 
			
		||||
@ -81,13 +96,19 @@ class CommentViewSet(viewsets.ModelViewSet):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# --- 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):
 | 
			
		||||
    """User's settings specific to aircox.
 | 
			
		||||
 | 
			
		||||
    Allow only to create and edit user's own settings.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    serializer_class = serializers.admin.UserSettingsSerializer
 | 
			
		||||
    serializer_class = serializers.UserSettingsSerializer
 | 
			
		||||
    permission_classes = (permissions.IsAuthenticated,)
 | 
			
		||||
 | 
			
		||||
    def get_serializer(self, instance=None, **kwargs):
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@ import tzlocal
 | 
			
		||||
 | 
			
		||||
from aircox.utils import to_seconds
 | 
			
		||||
 | 
			
		||||
from ..conf import settings
 | 
			
		||||
from .metadata import Metadata, Request
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -76,7 +77,7 @@ class PlaylistSource(Source):
 | 
			
		||||
        self.program = program
 | 
			
		||||
 | 
			
		||||
        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):
 | 
			
		||||
        """Get playlist's sounds queryset."""
 | 
			
		||||
@ -88,6 +89,7 @@ class PlaylistSource(Source):
 | 
			
		||||
 | 
			
		||||
    def write_playlist(self, playlist=[]):
 | 
			
		||||
        """Write playlist to file."""
 | 
			
		||||
        playlist = playlist or self.get_playlist()
 | 
			
		||||
        os.makedirs(os.path.dirname(self.path), exist_ok=True)
 | 
			
		||||
        with open(self.path, "w") as file:
 | 
			
		||||
            file.write("\n".join(playlist or []))
 | 
			
		||||
 | 
			
		||||
@ -95,6 +95,8 @@ class Streamer:
 | 
			
		||||
        data = render_to_string(
 | 
			
		||||
            self.template_name,
 | 
			
		||||
            {
 | 
			
		||||
                "dir": settings.get_dir(self.station),
 | 
			
		||||
                "log_file": settings.get_dir(self.station, "liquidsoap.log"),
 | 
			
		||||
                "station": self.station,
 | 
			
		||||
                "streamer": self,
 | 
			
		||||
            },
 | 
			
		||||
 | 
			
		||||
@ -53,13 +53,13 @@ class SourceSerializer(MetadataSerializer):
 | 
			
		||||
class PlaylistSerializer(SourceSerializer):
 | 
			
		||||
    program = serializers.CharField(source="program.id")
 | 
			
		||||
 | 
			
		||||
    url_name = "admin:api:streamer-playlist-detail"
 | 
			
		||||
    url_name = "streamer:api:streamer-playlist-detail"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class QueueSourceSerializer(SourceSerializer):
 | 
			
		||||
    queue = serializers.ListField(child=RequestSerializer(), source="requests")
 | 
			
		||||
 | 
			
		||||
    url_name = "admin:api:streamer-queue-detail"
 | 
			
		||||
    url_name = "streamer:api:streamer-queue-detail"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StreamerSerializer(BaseSerializer):
 | 
			
		||||
@ -69,7 +69,7 @@ class StreamerSerializer(BaseSerializer):
 | 
			
		||||
    playlists = serializers.ListField(child=PlaylistSerializer())
 | 
			
		||||
    queues = serializers.ListField(child=QueueSourceSerializer())
 | 
			
		||||
 | 
			
		||||
    url_name = "admin:api:streamer-detail"
 | 
			
		||||
    url_name = "streamer:api:streamer-detail"
 | 
			
		||||
 | 
			
		||||
    def get_url(self, obj, **kwargs):
 | 
			
		||||
        kwargs["pk"] = obj.station.pk
 | 
			
		||||
 | 
			
		||||
@ -80,7 +80,7 @@ end
 | 
			
		||||
{% block config %}
 | 
			
		||||
set("server.socket", true)
 | 
			
		||||
set("server.socket.path", "{{ streamer.socket_path }}")
 | 
			
		||||
set("log.file.path", "{{ station.path }}/liquidsoap.log")
 | 
			
		||||
set("log.file.path", "{{ log_file }}")
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block config_extras %}
 | 
			
		||||
 | 
			
		||||
@ -1,16 +1,13 @@
 | 
			
		||||
{% extends "admin/base_site.html" %}
 | 
			
		||||
{% comment %}Admin tools used to manage the streamer.{% endcomment %}
 | 
			
		||||
{% extends "aircox/dashboard/base.html" %}
 | 
			
		||||
{% 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 }}
 | 
			
		||||
<div id="app">
 | 
			
		||||
    <a-streamer api-url="{% url "admin:api:streamer-list" %}">
 | 
			
		||||
<div class="container">
 | 
			
		||||
    <a-streamer api-url="{% url "streamer:api:streamer-list" %}">
 | 
			
		||||
    <template v-slot="{streamer,streamers,sources,fetchStreamers,Sound}">
 | 
			
		||||
        <div class="navbar toolbar">
 | 
			
		||||
            <div class="navbar-start">
 | 
			
		||||
 | 
			
		||||
@ -1,33 +1,25 @@
 | 
			
		||||
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 . import viewsets
 | 
			
		||||
from .views import StreamerAdminView
 | 
			
		||||
from . import views, viewsets
 | 
			
		||||
 | 
			
		||||
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.register(
 | 
			
		||||
    streamer_prefix + "playlist",
 | 
			
		||||
    viewsets.PlaylistSourceViewSet,
 | 
			
		||||
    basename="streamer-playlist",
 | 
			
		||||
)
 | 
			
		||||
router.register(
 | 
			
		||||
    streamer_prefix + "queue",
 | 
			
		||||
    viewsets.QueueSourceViewSet,
 | 
			
		||||
    basename="streamer-queue",
 | 
			
		||||
)
 | 
			
		||||
router.register(prefix + "playlist", viewsets.PlaylistSourceViewSet, basename="streamer-playlist")
 | 
			
		||||
router.register(prefix + "queue", viewsets.QueueSourceViewSet, basename="streamer-queue")
 | 
			
		||||
router.register("streamer", viewsets.StreamerViewSet, basename="streamer")
 | 
			
		||||
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.views.generic import TemplateView
 | 
			
		||||
 | 
			
		||||
from aircox.views.admin import AdminMixin
 | 
			
		||||
from aircox.views.dashboard import DashboardBaseView
 | 
			
		||||
from .controllers import streamers
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StreamerAdminView(AdminMixin, TemplateView):
 | 
			
		||||
class StreamerView(DashboardBaseView, TemplateView):
 | 
			
		||||
    template_name = "aircox_streamer/streamer.html"
 | 
			
		||||
    title = _("Streamer Monitor")
 | 
			
		||||
    title = _("Streamer")
 | 
			
		||||
    streamers = streamers
 | 
			
		||||
 | 
			
		||||
    def dispatch(self, *args, **kwargs):
 | 
			
		||||
 | 
			
		||||
@ -43,6 +43,8 @@ class ControllerViewSet(viewsets.ViewSet):
 | 
			
		||||
        if station_pk is None:
 | 
			
		||||
            station_pk = self.request.station.pk
 | 
			
		||||
        self.streamers.fetch()
 | 
			
		||||
        if station_pk is None:
 | 
			
		||||
            return None
 | 
			
		||||
        if station_pk not in self.streamers:
 | 
			
		||||
            raise Http404("station not found")
 | 
			
		||||
        return self.streamers[station_pk]
 | 
			
		||||
@ -78,7 +80,7 @@ class StreamerViewSet(ControllerViewSet):
 | 
			
		||||
    def dispatch(self, request, *args, pk=None, **kwargs):
 | 
			
		||||
        if pk is not None:
 | 
			
		||||
            kwargs.setdefault("station_pk", pk)
 | 
			
		||||
        self.streamer = self.get_streamer(request, **kwargs)
 | 
			
		||||
        self.streamer = self.get_streamer(**kwargs)
 | 
			
		||||
        self.object = self.streamer
 | 
			
		||||
        return super().dispatch(request, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -27,7 +27,8 @@
 | 
			
		||||
    "eslint-plugin-vue": "^8.0.3",
 | 
			
		||||
    "sass": "^1.49.9",
 | 
			
		||||
    "sass-loader": "^12.6.0",
 | 
			
		||||
    "vue-cli": "^2.9.6"
 | 
			
		||||
    "vue-cli": "^2.9.6",
 | 
			
		||||
    "webpack-cli": "^5.1.4"
 | 
			
		||||
  },
 | 
			
		||||
  "eslintConfig": {
 | 
			
		||||
    "root": true,
 | 
			
		||||
 | 
			
		||||
@ -232,6 +232,8 @@ export default {
 | 
			
		||||
                                     : fetch(url, Model.getOptions()).then(d => d.json())
 | 
			
		||||
 | 
			
		||||
            promise = promise.then(items => {
 | 
			
		||||
                if(items.results)
 | 
			
		||||
                    items = items.results
 | 
			
		||||
                this.items = items.filter((i) => i) || []
 | 
			
		||||
                this.promise = null;
 | 
			
		||||
                this.move(0)
 | 
			
		||||
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							@ -184,9 +184,9 @@ THUMBNAIL_PROCESSORS = (
 | 
			
		||||
# Enabled applications
 | 
			
		||||
INSTALLED_APPS = (
 | 
			
		||||
    "radiocampus",
 | 
			
		||||
    "aircox_streamer.apps.AircoxStreamerConfig",
 | 
			
		||||
    "aircox.apps.AircoxConfig",
 | 
			
		||||
    "aircox.apps.AircoxAdminConfig",
 | 
			
		||||
    "aircox_streamer.apps.AircoxStreamerConfig",
 | 
			
		||||
    # Aircox dependencies
 | 
			
		||||
    "rest_framework",
 | 
			
		||||
    "django_filters",
 | 
			
		||||
 | 
			
		||||
@ -22,16 +22,14 @@ from django.urls import include, path
 | 
			
		||||
import aircox.urls
 | 
			
		||||
import aircox_streamer.urls
 | 
			
		||||
 | 
			
		||||
urlpatterns = (
 | 
			
		||||
    aircox.urls.urls
 | 
			
		||||
    + aircox_streamer.urls.urls
 | 
			
		||||
    + [
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    *aircox.urls.urls,
 | 
			
		||||
    path("streamer/", include((aircox_streamer.urls.urls, "aircox_streamer"), namespace="streamer")),
 | 
			
		||||
    path("admin/", admin.site.urls),
 | 
			
		||||
    path("accounts/", include("django.contrib.auth.urls")),
 | 
			
		||||
    path("ckeditor/", include("ckeditor_uploader.urls")),
 | 
			
		||||
    path("filer/", include("filer.urls")),
 | 
			
		||||
]
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
if settings.DEBUG:
 | 
			
		||||
    urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + static(
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
{% extends "aircox/base.html" %}
 | 
			
		||||
{% load static %}
 | 
			
		||||
 | 
			
		||||
{% block head_extra %}
 | 
			
		||||
{% block assets %}
 | 
			
		||||
{{ block.super }}
 | 
			
		||||
<style>
 | 
			
		||||
:root {
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,10 @@
 | 
			
		||||
Django~=4.1
 | 
			
		||||
djangorestframework~=3.13
 | 
			
		||||
django-model-utils>=4.2
 | 
			
		||||
Django~=5.0
 | 
			
		||||
djangorestframework~=3.14
 | 
			
		||||
django-model-utils>=4.3
 | 
			
		||||
django-filter~=22.1
 | 
			
		||||
 | 
			
		||||
django-content-editor~=6.3
 | 
			
		||||
django-filer~=2.2
 | 
			
		||||
django-filer~=3.1
 | 
			
		||||
django-honeypot~=1.0
 | 
			
		||||
django-taggit~=3.0
 | 
			
		||||
django-admin-sortable2~=2.1
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user