Compare commits
	
		
			148 Commits
		
	
	
		
			develop-1.
			...
			8208554c4b
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 8208554c4b | |||
| 87692c860b | |||
| a64c850efa | |||
| 881d518acb | |||
| 919cb06da8 | |||
| 8f1ec9cbc1 | |||
| ae176dc623 | |||
| 0512533244 | |||
| a2a399e531 | |||
| b28105c659 | |||
| 07d72d799d | |||
| 1d321a0de6 | |||
| 8a6f72ca83 | |||
| 0ee72f30c5 | |||
| 7841fed17d | |||
| 1bd4e03f02 | |||
| a24318bc84 | |||
| bda4efe336 | |||
| c3c748eebb | |||
| 3fb9e0d62a | |||
| 8d4b4c5896 | |||
| 1f716891ac | |||
| 70a55607a5 | |||
| f41cc3ce0c | |||
| 21f856e731 | |||
| d293eb4a00 | |||
| 3ad886764c | |||
| 024db5f307 | |||
| de858f45e8 | |||
| eaf453086d | |||
| 3c56dc8b53 | |||
| 44b9a608ee | |||
| eb5bdcf167 | |||
| c74ec6fb16 | |||
| c79f040fa1 | |||
| 7cdf44b901 | |||
| dff7b1cf8c | |||
| f55d747034 | |||
| 37ecf9875b | |||
| f8401c76e3 | |||
| 306eb20257 | |||
| 5ae85083a5 | |||
| 1ac83f1066 | |||
| 7e0e6e9652 | |||
| ab1b152a46 | |||
| 8821cd86c6 | |||
| 4ea93c9eff | |||
| 40ca2064d9 | |||
| e840fbabac | |||
| ff2a8ff6d4 | |||
| d33256edb8 | |||
| 10b9e9280f | |||
| ee7f301f44 | |||
| 64984d69d5 | |||
| 4f856c0705 | |||
| 8b4da52760 | |||
| aa171375e5 | |||
| 1674266890 | |||
| dd71f984ed | |||
| 0ba0f8ae72 | |||
| afc2e41bdb | |||
| bba4935791 | |||
| dab4146735 | |||
| 1aababe2ae | |||
| 0dd961e0bb | |||
| f9da318a38 | |||
| 26fa426416 | |||
| 71f4d2473e | |||
| 2e9ebaded2 | |||
| c6a4196319 | |||
| be224d0efb | |||
| 89f80ad103 | |||
| 6d556fcd5d | |||
| 4201d50f4b | |||
| 6c942f36fa | |||
| d51b9ee58b | |||
| 1a27ae2a76 | |||
| e5862ee59b | |||
| 8f88b15536 | |||
| 10dfe3811b | |||
| f71c201020 | |||
| 0812f3a0a1 | |||
| 269b29b2c1 | |||
| ad2ed17c34 | |||
| 9db69580e0 | |||
| 4ead6b154b | |||
| 811cc97e07 | |||
| b794e24d0c | |||
| df41885cca | |||
| 2a75608701 | |||
| e1cf455384 | |||
| 93e286fa62 | |||
| e3966ca5cb | |||
| c335ed9fb9 | |||
| ad90255570 | |||
| cab6cacd0b | |||
| 1475a80316 | |||
| b9148933f4 | |||
| 5bb52a9d67 | |||
| 8cf57c07b2 | |||
| 20aa3aba9d | |||
| d53cb3e935 | |||
| 25ceacdff9 | |||
| 0adcacf375 | |||
| c31d776504 | |||
| 69b77a675b | |||
| ac9b3c8ede | |||
| 825ed03dbd | |||
| 561914ee78 | |||
| ccea2a5ea6 | |||
| c52e87acd2 | |||
| 294c848415 | |||
| 1f6381bf07 | |||
| 73d8ff32d5 | |||
| 46a9008cda | |||
| eaa1e2412a | |||
| a3c21c64ed | |||
| 0e444f0502 | |||
| 4778803ee0 | |||
| 9c3eaf05c7 | |||
| f05e47af1c | |||
| 1de9548111 | |||
| 8202a9324c | |||
| f5ce00795e | |||
| 4e04cfae7e | |||
| d2ed8df2ac | |||
| 712ab223ba | |||
| ed9affbef6 | |||
| cb5a6a3ee8 | |||
| bc697bd4bd | |||
| d075fecbce | |||
| 0c07586787 | |||
| 9661e98a70 | |||
| 69d77e1d0c | |||
| 62ada47352 | |||
| 474016f776 | |||
| 6a21a9d094 | |||
| b4c12def13 | |||
| 36ae12af3d | |||
| 0a86d4e0a3 | |||
| a53aebb5b8 | |||
| 1af0348c89 | |||
| 8ab8ef5b1c | |||
| bf9da835b2 | |||
| 7b28149d7e | |||
| 87a2ee5a45 | |||
| ab231e9a89 | |||
| 1661601caf | 
@ -2,7 +2,6 @@ import os
 | 
			
		||||
 | 
			
		||||
import inspect
 | 
			
		||||
 | 
			
		||||
from bleach import sanitizer
 | 
			
		||||
from django.conf import settings as d_settings
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -180,10 +179,5 @@ class Settings(BaseSettings):
 | 
			
		||||
    ALLOW_COMMENTS = True
 | 
			
		||||
    """Allow comments."""
 | 
			
		||||
 | 
			
		||||
    # ---- bleach
 | 
			
		||||
    ALLOWED_TAGS = [*sanitizer.ALLOWED_TAGS, "br", "p", "h3", "h4", "h5"]
 | 
			
		||||
    ALLOWED_ATTRIBUTES = sanitizer.ALLOWED_ATTRIBUTES
 | 
			
		||||
    ALLOWED_PROTOCOLS = sanitizer.ALLOWED_PROTOCOLS
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
settings = Settings("AIRCOX")
 | 
			
		||||
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							@ -126,11 +126,11 @@ msgstr "type"
 | 
			
		||||
 | 
			
		||||
#: models/diffusion.py:137 models/log.py:131
 | 
			
		||||
msgid "Diffusion"
 | 
			
		||||
msgstr "Diffusion"
 | 
			
		||||
msgstr "Date de diffusion"
 | 
			
		||||
 | 
			
		||||
#: models/diffusion.py:138
 | 
			
		||||
msgid "Diffusions"
 | 
			
		||||
msgstr "Diffusions"
 | 
			
		||||
msgstr "Dates de diffusion"
 | 
			
		||||
 | 
			
		||||
#: models/diffusion.py:139
 | 
			
		||||
msgid "edit the diffusions' planification"
 | 
			
		||||
@ -325,7 +325,7 @@ msgstr "Page d'accueil"
 | 
			
		||||
 | 
			
		||||
#: models/page.py:291
 | 
			
		||||
msgid "Timetable"
 | 
			
		||||
msgstr "Horaires"
 | 
			
		||||
msgstr "Temps"
 | 
			
		||||
 | 
			
		||||
#: models/page.py:292
 | 
			
		||||
msgid "Programs list"
 | 
			
		||||
@ -780,7 +780,7 @@ msgstr "Utilisateurs"
 | 
			
		||||
#: templates/aircox/dashboard/user_list.html:12
 | 
			
		||||
msgid "Group and editors' changes will be visible only after page reload."
 | 
			
		||||
msgstr ""
 | 
			
		||||
"Les changement de groupe et d'éditeurs ne seront visibles qu'après le "
 | 
			
		||||
"Les changement de group et d'éditeurs ne seront visible qu'après le "
 | 
			
		||||
"rechargement de la page."
 | 
			
		||||
 | 
			
		||||
#: templates/aircox/dashboard/user_list.html:20
 | 
			
		||||
@ -811,7 +811,7 @@ msgstr "Membres"
 | 
			
		||||
 | 
			
		||||
#: templates/aircox/dashboard/widgets/tracklist_editor.html:15
 | 
			
		||||
msgid "Track list"
 | 
			
		||||
msgstr "Liste des morceaux"
 | 
			
		||||
msgstr "List des morceaux"
 | 
			
		||||
 | 
			
		||||
#: templates/aircox/diffusion_list.html:9
 | 
			
		||||
#, python-format
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
# Generated by Django 3.0.6 on 2020-05-26 12:57
 | 
			
		||||
 | 
			
		||||
import ckeditor.fields
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
@ -122,7 +123,7 @@ class Migration(migrations.Migration):
 | 
			
		||||
                ),
 | 
			
		||||
                (
 | 
			
		||||
                    "content",
 | 
			
		||||
                    models.TextField(blank=True, null=True, verbose_name="content"),
 | 
			
		||||
                    ckeditor.fields.RichTextField(blank=True, null=True, verbose_name="content"),
 | 
			
		||||
                ),
 | 
			
		||||
                ("pub_date", models.DateTimeField(blank=True, null=True)),
 | 
			
		||||
                (
 | 
			
		||||
@ -332,7 +333,7 @@ class Migration(migrations.Migration):
 | 
			
		||||
                    "active",
 | 
			
		||||
                    models.BooleanField(
 | 
			
		||||
                        default=True,
 | 
			
		||||
                        help_text="if not chemodels.onger active",
 | 
			
		||||
                        help_text="if not checked this program is no longer active",
 | 
			
		||||
                        verbose_name="active",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
@ -555,7 +556,7 @@ class Migration(migrations.Migration):
 | 
			
		||||
                ),
 | 
			
		||||
                (
 | 
			
		||||
                    "content",
 | 
			
		||||
                    models.TextField(blank=True, null=True, verbose_name="content"),
 | 
			
		||||
                    ckeditor.fields.RichTextField(blank=True, null=True, verbose_name="content"),
 | 
			
		||||
                ),
 | 
			
		||||
                (
 | 
			
		||||
                    "view",
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
# Generated by Django 3.1.1 on 2020-09-21 23:56
 | 
			
		||||
 | 
			
		||||
import ckeditor_uploader.fields
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
 | 
			
		||||
@ -29,6 +30,11 @@ class Migration(migrations.Migration):
 | 
			
		||||
            model_name="sound",
 | 
			
		||||
            name="embed",
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="page",
 | 
			
		||||
            name="content",
 | 
			
		||||
            field=ckeditor_uploader.fields.RichTextUploadingField(blank=True, null=True, verbose_name="content"),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="sound",
 | 
			
		||||
            name="program",
 | 
			
		||||
@ -41,4 +47,9 @@ class Migration(migrations.Migration):
 | 
			
		||||
            ),
 | 
			
		||||
            preserve_default=False,
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="staticpage",
 | 
			
		||||
            name="content",
 | 
			
		||||
            field=ckeditor_uploader.fields.RichTextUploadingField(blank=True, null=True, verbose_name="content"),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
@ -1,64 +0,0 @@
 | 
			
		||||
# Generated by Django 5.0.4 on 2024-05-27 12:40
 | 
			
		||||
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("aircox", "0027_remove_page_parent_remove_staticpage_parent_and_more"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterModelOptions(
 | 
			
		||||
            name="episodesound",
 | 
			
		||||
            options={"verbose_name": "Podcast", "verbose_name_plural": "Podcasts"},
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="diffusion",
 | 
			
		||||
            name="initial",
 | 
			
		||||
            field=models.ForeignKey(
 | 
			
		||||
                blank=True,
 | 
			
		||||
                limit_choices_to=models.Q(("initial__isnull", True), ("program", models.F("program"))),
 | 
			
		||||
                null=True,
 | 
			
		||||
                on_delete=django.db.models.deletion.SET_NULL,
 | 
			
		||||
                related_name="rerun_set",
 | 
			
		||||
                to="aircox.diffusion",
 | 
			
		||||
                verbose_name="rerun of",
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="log",
 | 
			
		||||
            name="source",
 | 
			
		||||
            field=models.CharField(
 | 
			
		||||
                blank=True, help_text="Identifier of the log's source.", max_length=64, null=True, verbose_name="source"
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="program",
 | 
			
		||||
            name="active",
 | 
			
		||||
            field=models.BooleanField(
 | 
			
		||||
                default=True, help_text="if not checked this program is no longer active", verbose_name="active"
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="schedule",
 | 
			
		||||
            name="initial",
 | 
			
		||||
            field=models.ForeignKey(
 | 
			
		||||
                blank=True,
 | 
			
		||||
                limit_choices_to=models.Q(("initial__isnull", True), ("program", models.F("program"))),
 | 
			
		||||
                null=True,
 | 
			
		||||
                on_delete=django.db.models.deletion.SET_NULL,
 | 
			
		||||
                related_name="rerun_set",
 | 
			
		||||
                to="aircox.schedule",
 | 
			
		||||
                verbose_name="rerun of",
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="sound",
 | 
			
		||||
            name="is_downloadable",
 | 
			
		||||
            field=models.BooleanField(
 | 
			
		||||
                default=False, help_text="Sound can be downloaded by website visitors.", verbose_name="downloadable"
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -16,7 +16,7 @@ __all__ = ("Episode",)
 | 
			
		||||
 | 
			
		||||
class EpisodeQuerySet(ProgramChildQuerySet):
 | 
			
		||||
    def with_podcasts(self):
 | 
			
		||||
        return self.filter(episodesound__sound__is_public=True, episodesound__sound__is_removed=False).distinct()
 | 
			
		||||
        return self.filter(episodesound__sound__is_public=True).distinct()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Episode(ChildPage):
 | 
			
		||||
@ -37,7 +37,7 @@ class Episode(ChildPage):
 | 
			
		||||
    @cached_property
 | 
			
		||||
    def podcasts(self):
 | 
			
		||||
        """Return serialized data about podcasts."""
 | 
			
		||||
        query = self.episodesound_set.available().public().order_by("-broadcast", "position")
 | 
			
		||||
        query = self.episodesound_set.all().public().order_by("-broadcast", "position")
 | 
			
		||||
        return self._to_podcasts(query)
 | 
			
		||||
 | 
			
		||||
    @cached_property
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import re
 | 
			
		||||
 | 
			
		||||
import bleach
 | 
			
		||||
from ckeditor_uploader.fields import RichTextUploadingField
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
from django.utils import timezone as tz
 | 
			
		||||
@ -12,7 +13,6 @@ from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from filer.fields.image import FilerImageField
 | 
			
		||||
from model_utils.managers import InheritanceQuerySet
 | 
			
		||||
 | 
			
		||||
from ..conf import settings
 | 
			
		||||
from .station import Station
 | 
			
		||||
 | 
			
		||||
__all__ = (
 | 
			
		||||
@ -99,7 +99,11 @@ class BasePage(Renderable, models.Model):
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
    )
 | 
			
		||||
    content = models.TextField(_("content"), blank=True, null=True)
 | 
			
		||||
    content = RichTextUploadingField(
 | 
			
		||||
        _("content"),
 | 
			
		||||
        blank=True,
 | 
			
		||||
        null=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    objects = BasePageQuerySet.as_manager()
 | 
			
		||||
 | 
			
		||||
@ -116,14 +120,6 @@ class BasePage(Renderable, models.Model):
 | 
			
		||||
        return "{}".format(self.title or self.pk)
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        if self.content:
 | 
			
		||||
            self.content = bleach.clean(
 | 
			
		||||
                self.content,
 | 
			
		||||
                tags=settings.ALLOWED_TAGS,
 | 
			
		||||
                attributes=settings.ALLOWED_ATTRIBUTES,
 | 
			
		||||
                protocols=settings.ALLOWED_PROTOCOLS,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        if not self.slug:
 | 
			
		||||
            self.slug = slugify(self.title)[:100]
 | 
			
		||||
            count = Page.objects.filter(slug__startswith=self.slug).count()
 | 
			
		||||
@ -169,6 +165,17 @@ class BasePage(Renderable, models.Model):
 | 
			
		||||
        headline[-1] += suffix
 | 
			
		||||
        return mark_safe(" ".join(headline))
 | 
			
		||||
 | 
			
		||||
    _url_re = re.compile(
 | 
			
		||||
        "((http|https)\:\/\/)?[a-zA-Z0-9\.\/\?\:@\-_=#]+\.([a-zA-Z]){2,6}([a-zA-Z0-9\.\&\/\?\:@\-_=#])*"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    @cached_property
 | 
			
		||||
    def display_content(self):
 | 
			
		||||
        if "<p>" in self.content:
 | 
			
		||||
            return self.content
 | 
			
		||||
        content = self._url_re.sub(r'<a href="\1" target="new">\1</a>', self.content)
 | 
			
		||||
        return content.replace("\n\n", "\n").replace("\n", "<br>")
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def get_init_kwargs_from(cls, page, **kwargs):
 | 
			
		||||
        kwargs.setdefault("cover", page.cover)
 | 
			
		||||
 | 
			
		||||
@ -123,16 +123,6 @@ class Program(Page):
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.title
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        if not self.editors_group_id:
 | 
			
		||||
            from aircox import permissions
 | 
			
		||||
 | 
			
		||||
            saved = permissions.program.init(self)
 | 
			
		||||
            if saved:
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
        super().save()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ProgramChildQuerySet(PageQuerySet):
 | 
			
		||||
    def station(self, station=None, id=None):
 | 
			
		||||
 | 
			
		||||
@ -75,10 +75,9 @@ class Rerun(models.Model):
 | 
			
		||||
            raise ValidationError({"initial": _("rerun must happen after original")})
 | 
			
		||||
 | 
			
		||||
    def save_rerun(self):
 | 
			
		||||
        if not self.program_id:
 | 
			
		||||
            self.program = self.initial.program
 | 
			
		||||
        if self.program != self.initial.program:
 | 
			
		||||
        if self.program and self.program != self.initial.program:
 | 
			
		||||
            raise ValidationError("Program for the rerun should be the same")
 | 
			
		||||
        self.program = self.initial.program
 | 
			
		||||
 | 
			
		||||
    def save_initial(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,6 @@ class PagePermissions:
 | 
			
		||||
    """Handles obj permissions initialization of page subclass."""
 | 
			
		||||
 | 
			
		||||
    model = None
 | 
			
		||||
    # TODO: move values to subclass
 | 
			
		||||
    groups = ({"label": _("editors"), "field": "editors_group_id", "perms": ["update"]},)
 | 
			
		||||
    """Groups informations initialized."""
 | 
			
		||||
    groups_name_format = "{obj.title}: {group_label}"
 | 
			
		||||
@ -40,21 +39,19 @@ class PagePermissions:
 | 
			
		||||
        if user.is_superuser:
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        perm = self.perms_codename_format.format(self=self, perm=perm, obj=obj)
 | 
			
		||||
        perm = self.perms_codename_format.format(self=self, perm=perm)
 | 
			
		||||
        return user.has_perm(perm)
 | 
			
		||||
 | 
			
		||||
    def init(self, obj, model=None):
 | 
			
		||||
        """Initialize permissions for the provided obj.
 | 
			
		||||
 | 
			
		||||
        Return True if group  or permission have been created (`obj` has
 | 
			
		||||
        thus been saved).
 | 
			
		||||
        """
 | 
			
		||||
        """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)
 | 
			
		||||
@ -68,8 +65,6 @@ class PagePermissions:
 | 
			
		||||
        for group, infos in created_groups:
 | 
			
		||||
            self.init_perms(obj, group, infos)
 | 
			
		||||
 | 
			
		||||
        return updated
 | 
			
		||||
 | 
			
		||||
    def init_group(self, obj, infos):
 | 
			
		||||
        name = self.groups_name_format.format(obj=obj, group_label=infos["label"])
 | 
			
		||||
        return Group.objects.get_or_create(name=name)
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@ -136,9 +136,9 @@ Usefull context:
 | 
			
		||||
 | 
			
		||||
                {% block content-container %}
 | 
			
		||||
                {% if page and page.content %}
 | 
			
		||||
                <section class="container no-reset content page-content">
 | 
			
		||||
                <section class="container content page-content">
 | 
			
		||||
                    {% block content %}
 | 
			
		||||
                    {{ page.content|safe }}
 | 
			
		||||
                        {{ page.display_content|safe }}
 | 
			
		||||
                    {% endblock %}
 | 
			
		||||
                </section>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
 | 
			
		||||
@ -58,7 +58,7 @@
 | 
			
		||||
 | 
			
		||||
{% if podcasts %}
 | 
			
		||||
<section class="container">
 | 
			
		||||
    <h2 class="title">{% translate "Last podcasts" %}</h2>
 | 
			
		||||
    <h2 class="title is-3 p-2">{% translate "Last podcasts" %}</h2>
 | 
			
		||||
    {% include "./widgets/carousel.html" with objects=podcasts url_name="podcast-list" url_label=_("All podcasts") %}
 | 
			
		||||
</section>
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
@ -18,12 +18,6 @@ aircox.labels = {% inline_labels %}
 | 
			
		||||
{% endif %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block title-container %}
 | 
			
		||||
{{ block.super }}
 | 
			
		||||
{% block page-actions %}
 | 
			
		||||
    {% include "aircox/widgets/page_actions.html" %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content-container %}
 | 
			
		||||
<a-select-file ref="cover-select"
 | 
			
		||||
@ -108,11 +102,8 @@ aircox.labels = {% inline_labels %}
 | 
			
		||||
    <div>
 | 
			
		||||
        <div class="field {% if field.name != "content" %}is-horizontal{% endif %}">
 | 
			
		||||
            <label class="label">{{ field.label }}</label>
 | 
			
		||||
            <div class="control clear-unset no-reset">
 | 
			
		||||
                <a-editor name="{{ field.name }}" initial="{{ field.value }}"/>
 | 
			
		||||
                {% comment %}
 | 
			
		||||
            <div class="control clear-unset">
 | 
			
		||||
                <textarea name="{{ field.name }}" class="is-fullwidth height-25">{{ field.value|default:""|striptags|safe }}</textarea>
 | 
			
		||||
                {% endcomment %}
 | 
			
		||||
            </div>
 | 
			
		||||
            <p class="help">{{ field.help_text }}</p>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
@ -2,12 +2,10 @@
 | 
			
		||||
{% comment %}Detail page of a show{% endcomment %}
 | 
			
		||||
{% load i18n aircox %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
{{ block.super }}
 | 
			
		||||
 | 
			
		||||
{% block content-container %}
 | 
			
		||||
{% with schedules=object.schedule_set.all %}
 | 
			
		||||
{% if object.active and schedules %}
 | 
			
		||||
<header class="schedules mt-3">
 | 
			
		||||
{% if schedules %}
 | 
			
		||||
<header class="container schedules">
 | 
			
		||||
    {% for schedule in schedules %}
 | 
			
		||||
    <div class="schedule">
 | 
			
		||||
        <div class="heading">
 | 
			
		||||
@ -35,13 +33,8 @@
 | 
			
		||||
{% endif %}
 | 
			
		||||
{% endwith %}
 | 
			
		||||
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{% block main %}
 | 
			
		||||
{{ block.super }}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{% if episodes %}
 | 
			
		||||
<section class="container">
 | 
			
		||||
    <h2 class="title is-2">{% translate "Last Episodes" %}</h2>
 | 
			
		||||
 | 
			
		||||
@ -58,8 +58,8 @@
 | 
			
		||||
 | 
			
		||||
{% block actions %}
 | 
			
		||||
{{ block.super }}
 | 
			
		||||
{% if object.episodesound_set.available.public.count %}
 | 
			
		||||
<button type="button" class="button action" @click="player.playButtonClick($event)"
 | 
			
		||||
{% if object.sound_set.count %}
 | 
			
		||||
<button class="button action" @click="player.playButtonClick($event)"
 | 
			
		||||
        data-sounds="{{ object.podcasts|json }}">
 | 
			
		||||
    <span class="icon is-small">
 | 
			
		||||
        <span class="fas fa-play"></span>
 | 
			
		||||
 | 
			
		||||
@ -2,24 +2,24 @@
 | 
			
		||||
 | 
			
		||||
{% block user-actions-container %}
 | 
			
		||||
{% if user.is_authenticated %}
 | 
			
		||||
{{ object.get_status_display|capfirst }}
 | 
			
		||||
{{ object.get_status_display }}
 | 
			
		||||
 | 
			
		||||
{% if object.pub_date %}
 | 
			
		||||
    ({{ object.pub_date|date:"d/m/Y H:i" }})
 | 
			
		||||
{% endif %}
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
{% if user.is_authenticated %}
 | 
			
		||||
{% if user.is_authenticated and can_edit %}
 | 
			
		||||
{% with request.resolver_match.view_name as view_name %}
 | 
			
		||||
   
 | 
			
		||||
  {% if request.path != object.get_absolute_url %}
 | 
			
		||||
  {% if "-edit" in view_name %}
 | 
			
		||||
  <a href="{% url view_name|detail_view page.slug %}" target="_self" title="{% translate 'View' %} {{ page }}">
 | 
			
		||||
      <span class="icon">
 | 
			
		||||
          <i class="fa-regular fa-eye"></i>
 | 
			
		||||
      </span>
 | 
			
		||||
      <span>{% translate 'View' %} </span>
 | 
			
		||||
  </a>
 | 
			
		||||
  {% elif can_edit %}
 | 
			
		||||
  {% else %}
 | 
			
		||||
  <a href="{% url view_name|edit_view page.pk %}" target="_self" title="{% translate 'Edit' %} {{ page }}">
 | 
			
		||||
      <span class="icon">
 | 
			
		||||
          <i class="fa-solid fa-pencil"></i>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										45
									
								
								aircox/tests/test_admin_site.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								aircox/tests/test_admin_site.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,45 @@
 | 
			
		||||
from django.urls import path, reverse
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
import pytest
 | 
			
		||||
 | 
			
		||||
from aircox import admin_site, urls as _urls
 | 
			
		||||
from .conftest import req_factory
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Just for code quality: urls module is required because we need some
 | 
			
		||||
# url resolvers to be registered in order to run tests.
 | 
			
		||||
_urls
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def site():
 | 
			
		||||
    return admin_site.AdminSite()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestAdminSite:
 | 
			
		||||
    @pytest.mark.django_db
 | 
			
		||||
    def test_each_context(self, site, staff_user):
 | 
			
		||||
        req = req_factory.get("admin/test")
 | 
			
		||||
        req.user = staff_user
 | 
			
		||||
        context = site.each_context(req)
 | 
			
		||||
        assert "programs" in context
 | 
			
		||||
        assert "diffusions" in context
 | 
			
		||||
        assert "comments" in context
 | 
			
		||||
 | 
			
		||||
    def test_get_urls(self, site):
 | 
			
		||||
        extra_url = path("test/path", lambda *_, **kw: _)
 | 
			
		||||
        site.extra_urls.append(extra_url)
 | 
			
		||||
        urls = site.get_urls()
 | 
			
		||||
        assert extra_url in urls
 | 
			
		||||
 | 
			
		||||
    def test_get_tools(self, site):
 | 
			
		||||
        tools = site.get_tools()
 | 
			
		||||
        tools = dict(tools)
 | 
			
		||||
        assert tools == {
 | 
			
		||||
            _("Statistics"): reverse("admin:tools-stats"),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def test_route_view(self, site):
 | 
			
		||||
        # TODO
 | 
			
		||||
        pass
 | 
			
		||||
							
								
								
									
										22
									
								
								aircox/tests/test_profile.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								aircox/tests/test_profile.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
			
		||||
import pytest
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.django_db()
 | 
			
		||||
def test_authenticate(user, client, program):
 | 
			
		||||
    r = client.get(reverse("login"))
 | 
			
		||||
    assert r.status_code == 200
 | 
			
		||||
    assert b"id_username" in r.content
 | 
			
		||||
    r = client.post(reverse("login"), kwargs={"username": "foo", "password": "bar"})
 | 
			
		||||
    assert b"errorlist" in r.content
 | 
			
		||||
    assert client.login(username="user1", password="bar")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.django_db()
 | 
			
		||||
def test_profile_programs(user, client, program):
 | 
			
		||||
    client.force_login(user)
 | 
			
		||||
    r = client.get(reverse("profile"))
 | 
			
		||||
    assert program.title not in r.content.decode("utf-8")
 | 
			
		||||
    user.groups.add(program.editors)
 | 
			
		||||
    r = client.get(reverse("profile"))
 | 
			
		||||
    assert program.title in r.content.decode("utf-8")
 | 
			
		||||
							
								
								
									
										51
									
								
								aircox/tests/test_program.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								aircox/tests/test_program.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,51 @@
 | 
			
		||||
# FIXME: this should be cleaner
 | 
			
		||||
from itertools import chain
 | 
			
		||||
import json
 | 
			
		||||
import pytest
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.django_db()
 | 
			
		||||
def test_edit_program(user, client, program):
 | 
			
		||||
    client.force_login(user)
 | 
			
		||||
    response = client.get(reverse("program-detail", kwargs={"slug": program.slug}))
 | 
			
		||||
    assert response.status_code == 200
 | 
			
		||||
    assert "🖉 ".encode() not in response.content
 | 
			
		||||
    user.groups.add(program.editors)
 | 
			
		||||
    response = client.get(reverse("program-detail", kwargs={"slug": program.slug}))
 | 
			
		||||
    assert "🖉 ".encode() in response.content
 | 
			
		||||
    assert b"foobar" not in response.content
 | 
			
		||||
    response = client.post(reverse("program-edit", kwargs={"pk": program.pk}), {"content": "foobar"})
 | 
			
		||||
    response = client.get(reverse("program-detail", kwargs={"slug": program.slug}))
 | 
			
		||||
    assert b"foobar" in response.content
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.django_db()
 | 
			
		||||
def test_edit_tracklist(user, client, program, episode, tracks):
 | 
			
		||||
    user.groups.add(program.editors)
 | 
			
		||||
    client.force_login(user)
 | 
			
		||||
    episode.status = 0x10  # published
 | 
			
		||||
    episode.save()
 | 
			
		||||
    r = client.get(reverse("program-detail", kwargs={"slug": episode.program.slug}))
 | 
			
		||||
    assert r.status_code == 200
 | 
			
		||||
    r = client.get(reverse("episode-detail", kwargs={"slug": episode.slug}))
 | 
			
		||||
    assert r.status_code == 200
 | 
			
		||||
    r2 = client.get(reverse("episode-edit", kwargs={"pk": episode.pk}))
 | 
			
		||||
    assert r2.status_code == 200
 | 
			
		||||
 | 
			
		||||
    tracklist = [t.id for t in episode.track_set.all().order_by("position")]
 | 
			
		||||
    tracklist_details_reversed = [(t.id, t.artist, t.title) for t in episode.track_set.all().order_by("-position")]
 | 
			
		||||
    tracklist_details_reversed = list(chain(*tracklist_details_reversed))
 | 
			
		||||
    data = """{{"website": [""], "content": ["foobar"], "new_podcast": [""], "form-TOTAL_FORMS": ["3"],
 | 
			
		||||
    "form-INITIAL_FORMS": ["3"], "form-MIN_NUM_FORMS": ["0"], "form-MAX_NUM_FORMS": ["1000"], "form-0-position": ["0"],
 | 
			
		||||
    "form-0-id": ["{}"], "form-0-": ["", "", "", "", "", ""], "form-0-artist": ["{}"], "form-0-title": ["{}"],
 | 
			
		||||
    "form-0-tags": [""], "form-0-album": [""], "form-0-year": [""], "form-1-position": ["1"], "form-1-id": ["{}"],
 | 
			
		||||
    "form-1-": ["", "", "", "", "", ""], "form-1-artist": ["{}"], "form-1-title": ["{}"], "form-1-tags": [""],
 | 
			
		||||
    "form-1-album": [""], "form-1-year": [""], "form-2-position": ["2"], "form-2-id": ["{}"], "form-2-": ["", "", "",
 | 
			
		||||
    "", "", ""], "form-2-artist": ["{}"], "form-2-title": ["{}"], "form-2-tags": [""], "form-2-album": [""],
 | 
			
		||||
    "form-2-year": [""]}}""".format(
 | 
			
		||||
        *tracklist_details_reversed
 | 
			
		||||
    )
 | 
			
		||||
    r = client.post(reverse("episode-edit", kwargs={"pk": episode.pk}), json.loads(data), follow=True)
 | 
			
		||||
    assert r.status_code == 200
 | 
			
		||||
    assert set(episode.track_set.all().values_list("id", flat=True)) == set(tracklist)
 | 
			
		||||
@ -44,7 +44,6 @@ class TestBaseView:
 | 
			
		||||
            "station": station,
 | 
			
		||||
            "page": None,  # get_page() returns None
 | 
			
		||||
            "model": base_view.model,
 | 
			
		||||
            "nav_menu": [],
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -80,7 +80,7 @@ class TestGetDateMixin:
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_get_calls_get_date(self, date_mixin):
 | 
			
		||||
        date_mixin.get_date = lambda *_: today
 | 
			
		||||
        date_mixin.get_date = lambda: today
 | 
			
		||||
        date_mixin.get()
 | 
			
		||||
        assert date_mixin.date == today
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -29,8 +29,8 @@ class TimeTableView(GetDateMixin, BaseDiffusionListView):
 | 
			
		||||
    attach_to_value = StaticPage.Target.TIMETABLE
 | 
			
		||||
    template_name = "aircox/timetable_list.html"
 | 
			
		||||
 | 
			
		||||
    def get_date(self, param="date"):
 | 
			
		||||
        date = super().get_date(param)
 | 
			
		||||
    def get_date(self):
 | 
			
		||||
        date = super().get_date()
 | 
			
		||||
        return date if date is not None else datetime.date.today()
 | 
			
		||||
 | 
			
		||||
    def get_logs(self, date):
 | 
			
		||||
 | 
			
		||||
@ -55,9 +55,6 @@ class EpisodeUpdateView(UserPassesTestMixin, VueFormDataMixin, PageUpdateView):
 | 
			
		||||
    form_class = forms.EpisodeForm
 | 
			
		||||
    template_name = "aircox/episode_form.html"
 | 
			
		||||
 | 
			
		||||
    def can_edit(self, obj):
 | 
			
		||||
        return self.test_func()
 | 
			
		||||
 | 
			
		||||
    def test_func(self):
 | 
			
		||||
        obj = self.get_object()
 | 
			
		||||
        return permissions.program.can(self.request.user, "update", obj)
 | 
			
		||||
 | 
			
		||||
@ -126,8 +126,8 @@ class LogListAPIView(LogListMixin, BaseAPIView, ListAPIView):
 | 
			
		||||
            self.min_date = tz.now() - tz.timedelta(minutes=30)
 | 
			
		||||
        return date
 | 
			
		||||
 | 
			
		||||
    def get_object_list(self, logs, *args, **kwargs):
 | 
			
		||||
        return [LogInfo(obj) for obj in super().get_object_list(logs, *args, **kwargs)]
 | 
			
		||||
    def get_object_list(self, logs, full):
 | 
			
		||||
        return [LogInfo(obj) for obj in super().get_object_list(logs, full)]
 | 
			
		||||
 | 
			
		||||
    def get_serializer(self, queryset, *args, **kwargs):
 | 
			
		||||
        full = bool(self.request.GET.get("full"))
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@ class GetDateMixin:
 | 
			
		||||
    date = None
 | 
			
		||||
    redirect_date_url = None
 | 
			
		||||
 | 
			
		||||
    def get_date(self, param="date"):
 | 
			
		||||
    def get_date(self, param):
 | 
			
		||||
        date = self.request.GET.get(param)
 | 
			
		||||
        return str_to_date(date, "-") if date else self.kwargs[param] if param in self.kwargs else None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -33,17 +33,6 @@ def attach(cls):
 | 
			
		||||
    return cls
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CanEditMixin:
 | 
			
		||||
    """Add context 'can_edit' set to True when object is editable by user."""
 | 
			
		||||
 | 
			
		||||
    def can_edit(self, object):
 | 
			
		||||
        """Return True if user can edit current page."""
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        return super().get_context_data(can_edit=self.can_edit(self.object), **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BasePageMixin:
 | 
			
		||||
    category = None
 | 
			
		||||
 | 
			
		||||
@ -167,12 +156,16 @@ class PageListView(FiltersMixin, BasePageListView):
 | 
			
		||||
        return super().get_context_data(**kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PageDetailView(CanEditMixin, BasePageDetailView):
 | 
			
		||||
class PageDetailView(BasePageDetailView):
 | 
			
		||||
    """Base view class for pages."""
 | 
			
		||||
 | 
			
		||||
    template_name = None
 | 
			
		||||
    context_object_name = "page"
 | 
			
		||||
 | 
			
		||||
    def can_edit(self, object):
 | 
			
		||||
        """Return True if user can edit current page."""
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    def get_template_names(self):
 | 
			
		||||
        return super().get_template_names() + ["aircox/page_detail.html"]
 | 
			
		||||
 | 
			
		||||
@ -192,6 +185,7 @@ class PageDetailView(CanEditMixin, BasePageDetailView):
 | 
			
		||||
            if related:
 | 
			
		||||
                related = related[: self.related_count]
 | 
			
		||||
            kwargs["related_objects"] = related
 | 
			
		||||
        kwargs["can_edit"] = self.can_edit(self.object)
 | 
			
		||||
        return super().get_context_data(**kwargs)
 | 
			
		||||
 | 
			
		||||
    def get_comment_form(self):
 | 
			
		||||
@ -216,7 +210,7 @@ class PageDetailView(CanEditMixin, BasePageDetailView):
 | 
			
		||||
        return self.get(request, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PageCreateView(CanEditMixin, BaseView, CreateView):
 | 
			
		||||
class PageCreateView(BaseView, CreateView):
 | 
			
		||||
    def get_page(self):
 | 
			
		||||
        return self.object
 | 
			
		||||
 | 
			
		||||
@ -224,7 +218,7 @@ class PageCreateView(CanEditMixin, BaseView, CreateView):
 | 
			
		||||
        return self.request.path
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PageUpdateView(CanEditMixin, BaseView, UpdateView):
 | 
			
		||||
class PageUpdateView(BaseView, UpdateView):
 | 
			
		||||
    def get_page(self):
 | 
			
		||||
        return self.object
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -71,9 +71,6 @@ class ProgramCreateView(PermissionRequiredMixin, ProgramEditMixin, page.PageCrea
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ProgramUpdateView(UserPassesTestMixin, ProgramEditMixin, page.PageUpdateView):
 | 
			
		||||
    def can_edit(self, obj):
 | 
			
		||||
        return self.test_func()
 | 
			
		||||
 | 
			
		||||
    def test_func(self):
 | 
			
		||||
        obj = self.get_object()
 | 
			
		||||
        return permissions.program.can(self.request.user, "update", obj)
 | 
			
		||||
 | 
			
		||||
@ -6,8 +6,8 @@ to:
 | 
			
		||||
- cancels Diffusions that have an archive but could not have been played;
 | 
			
		||||
- run Liquidsoap
 | 
			
		||||
"""
 | 
			
		||||
from datetime import timezone
 | 
			
		||||
import time
 | 
			
		||||
import zoneinfo
 | 
			
		||||
 | 
			
		||||
from argparse import RawTextHelpFormatter
 | 
			
		||||
 | 
			
		||||
@ -19,7 +19,7 @@ from aircox_streamer.controllers import Monitor, Streamer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# force using UTC
 | 
			
		||||
tz.activate(zoneinfo.ZoneInfo("UTC"))
 | 
			
		||||
tz.activate(timezone.utc)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,7 @@
 | 
			
		||||
            <small v-if="source.isPaused || source.isPlaying">([[ source.remainingString ]])</small>
 | 
			
		||||
 | 
			
		||||
            <a v-if="source.data.program !== undefined"
 | 
			
		||||
               :href="'{% url 'program-edit' "$$" %}'.replace('$$', source.data.program)"
 | 
			
		||||
               :href="'{% url 'aircox:program_edit' "$$" %}'.replace('$$', source.data.program)"
 | 
			
		||||
               title="{% translate "Edit program" %}">
 | 
			
		||||
                <span class="icon">
 | 
			
		||||
                    <span class="fas fa-edit"></span>
 | 
			
		||||
 | 
			
		||||
@ -66,7 +66,7 @@ class FakeSocket:
 | 
			
		||||
# -- models
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def station():
 | 
			
		||||
    station = models.Station(name="test", default=True, active=True)
 | 
			
		||||
    station = models.Station(name="test", path=working_dir, default=True, active=True)
 | 
			
		||||
    station.save()
 | 
			
		||||
    return station
 | 
			
		||||
 | 
			
		||||
@ -77,6 +77,7 @@ def stations(station):
 | 
			
		||||
        models.Station(
 | 
			
		||||
            name=f"test-{i}",
 | 
			
		||||
            slug=f"test-{i}",
 | 
			
		||||
            path=working_dir,
 | 
			
		||||
            default=(i == 0),
 | 
			
		||||
            active=True,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -20,11 +20,6 @@
 | 
			
		||||
    "vue": "^3.4.21"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@tiptap/extension-link": "^2.3.0",
 | 
			
		||||
    "@tiptap/extension-underline": "^2.3.0",
 | 
			
		||||
    "@tiptap/pm": "^2.3.0",
 | 
			
		||||
    "@tiptap/starter-kit": "^2.3.0",
 | 
			
		||||
    "@tiptap/vue-3": "^2.3.0",
 | 
			
		||||
    "@vitejs/plugin-vue": "^5.0.4",
 | 
			
		||||
    "bulma": "^0.9.4",
 | 
			
		||||
    "eslint": "^7.32.0",
 | 
			
		||||
 | 
			
		||||
@ -1,132 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <input ref="input" type="hidden" :name="name" :value="value"/>
 | 
			
		||||
    <div class="">
 | 
			
		||||
        <template v-for="group, index in menu" :key="index">
 | 
			
		||||
            <div class="button-group d-inline-block mr-3">
 | 
			
		||||
                <template v-for="info, index in group" :key="index">
 | 
			
		||||
                    <button type="button" class="button square smaller" :title="info.label" @click="edit(info.action, ...(info.args || []))">
 | 
			
		||||
                        <span class="icon"><i :class="info.icon"/></span>
 | 
			
		||||
                    </button>
 | 
			
		||||
                </template>
 | 
			
		||||
            </div>
 | 
			
		||||
        </template>
 | 
			
		||||
        <div class="button-group d-inline-block">
 | 
			
		||||
            <div class="dropdown is-hoverable">
 | 
			
		||||
                <div class="dropdown-trigger">
 | 
			
		||||
                    <button type="button" class="button square smaller">
 | 
			
		||||
                        <span class="icon"><i class="fa fa-link"/></span>
 | 
			
		||||
                    </button>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="dropdown-menu" style="min-width: 20rem; margin-top: -0.2rem;">
 | 
			
		||||
                    <div class="dropdown-content p-3">
 | 
			
		||||
                        <div class="field">
 | 
			
		||||
                            <label class="label">Lien</label>
 | 
			
		||||
                            <div class="control">
 | 
			
		||||
                                <input ref="link-url" type="text" class="input" placeholder="lien"/>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="has-text-right">
 | 
			
		||||
                            <button type="button" class="button secondary"
 | 
			
		||||
                                @click="edit('setLink', {href:$refs['link-url'].value})">
 | 
			
		||||
                                Ajouter le lien
 | 
			
		||||
                            </button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <button type="button" class="button square smaller" title="Remove link" @click="edit('unsetLink')">
 | 
			
		||||
                <span class="icon"><i class="fa fa-link-slash"/></span>
 | 
			
		||||
            </button>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <editor-content class="editor" v-if="editor" :editor="editor" />
 | 
			
		||||
</template>
 | 
			
		||||
<style>
 | 
			
		||||
.editor .tiptap {
 | 
			
		||||
    border: 1px black solid;
 | 
			
		||||
    padding: 0.3em;
 | 
			
		||||
}
 | 
			
		||||
.editor .tiptap ul, .editor .tiptap ol {
 | 
			
		||||
    margin-left: 1.3em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.editor .tiptap ul { list-style: disc }
 | 
			
		||||
</style>
 | 
			
		||||
<script>
 | 
			
		||||
import { Editor, EditorContent } from '@tiptap/vue-3'
 | 
			
		||||
import StarterKit from '@tiptap/starter-kit'
 | 
			
		||||
import Underline from '@tiptap/extension-underline'
 | 
			
		||||
import Link from '@tiptap/extension-link'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    components: {EditorContent},
 | 
			
		||||
    props: {
 | 
			
		||||
        config: {type: Object, default: (() => {})},
 | 
			
		||||
        //! Input field name.
 | 
			
		||||
        name: String,
 | 
			
		||||
        //! Initial input value
 | 
			
		||||
        initial: String,
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            editor: null,
 | 
			
		||||
            menu: [
 | 
			
		||||
                [
 | 
			
		||||
                    {label: "Bold", icon: "fa fa-bold", action: "toggleBold" },
 | 
			
		||||
                    {label: "Italic", icon: "fa fa-italic", action: "toggleItalic" },
 | 
			
		||||
                    {label: "Underline", icon: "fa fa-underline", action: "toggleUnderline" },
 | 
			
		||||
                    {label: "Strike", icon: "fa fa-strikethrough", action: "toggleStrike" },
 | 
			
		||||
                ],[
 | 
			
		||||
                    {label: "List", icon: "fa fa-list", action: "toggleBulletList" },
 | 
			
		||||
                    {label: "Ordered List", icon: "fa fa-list-ol", action: "toggleOrderedList" },
 | 
			
		||||
                ],[
 | 
			
		||||
                    {label: "Heading 1", icon: "fa fa-h", action: "setHeading", args: [{level:3}] },
 | 
			
		||||
                    {label: "Heading 2", icon: "fa fa-h smaller", action: "toggleHeading", args: [{level:4}] },
 | 
			
		||||
                    // {label: "Heading 3", icon: "fa fa-h small", action: "toggleHeading", args: [{level:5}] },
 | 
			
		||||
                ],
 | 
			
		||||
            ]
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    computed: {
 | 
			
		||||
        value() { return this.editor && this.editor.getHTML() },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    methods: {
 | 
			
		||||
        chain(action, ...args) {
 | 
			
		||||
            let chain = this.editor.chain().focus()
 | 
			
		||||
            return chain[action](...args)
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        edit(action, ...args) {
 | 
			
		||||
            this.chain(action, ...args).run()
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        setLink() {
 | 
			
		||||
            this.edit("setLink", {href: this.$refs['link-url']})
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    mounted() {
 | 
			
		||||
        this.editor = new Editor({
 | 
			
		||||
          content: this.initial || "",
 | 
			
		||||
          injectCss: false,
 | 
			
		||||
          extensions: [
 | 
			
		||||
            StarterKit.configure({
 | 
			
		||||
                heading: {
 | 
			
		||||
                    levels: [3, 4, 5]
 | 
			
		||||
                }
 | 
			
		||||
            }),
 | 
			
		||||
            Underline,
 | 
			
		||||
            Link.configure({autolink: true}),
 | 
			
		||||
          ],
 | 
			
		||||
        })
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    beforeUnmount() {
 | 
			
		||||
        this.editor.destroy()
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
@ -16,7 +16,7 @@
 | 
			
		||||
</template>
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
    emits: ['select', 'unselect', 'move', 'remove'],
 | 
			
		||||
    emits: ['select', 'unselect', 'move'],
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            selectedIndex: this.defaultIndex,
 | 
			
		||||
@ -50,16 +50,11 @@ export default {
 | 
			
		||||
        findIndex(pred) { return this.set.findIndex(pred) },
 | 
			
		||||
 | 
			
		||||
        remove(index, select=false) {
 | 
			
		||||
            const item = this.set.get(index)
 | 
			
		||||
            if(!item)
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
            this.set.remove(index);
 | 
			
		||||
            if(index < this.selectedIndex)
 | 
			
		||||
                this.selectedIndex--;
 | 
			
		||||
            if(select && this.selectedIndex == index)
 | 
			
		||||
                this.select(index)
 | 
			
		||||
            this.$emit('remove', {index, item, set: this.set})
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        select(index) {
 | 
			
		||||
 | 
			
		||||
@ -274,6 +274,7 @@ export default {
 | 
			
		||||
            if(event.type == 'ended' && (!this.playlist || this.playlist.selectNext() == -1))
 | 
			
		||||
                this.play();
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    mounted() {
 | 
			
		||||
 | 
			
		||||
@ -29,7 +29,7 @@ import ASoundItem from './ASoundItem';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    extends: AList,
 | 
			
		||||
    emits: [...AList.emits],
 | 
			
		||||
    emits: [...AList.emits, 'remove'],
 | 
			
		||||
    components: { ASoundItem },
 | 
			
		||||
 | 
			
		||||
    props: {
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,6 @@ import AStreamer from './AStreamer.vue'
 | 
			
		||||
import AFormSet from './AFormSet.vue'
 | 
			
		||||
import ATrackListEditor from './ATrackListEditor.vue'
 | 
			
		||||
import ASoundListEditor from './ASoundListEditor.vue'
 | 
			
		||||
import AEditor from './AEditor.vue'
 | 
			
		||||
 | 
			
		||||
import AManyToManyEdit from "./AManyToManyEdit.vue"
 | 
			
		||||
 | 
			
		||||
@ -16,7 +15,7 @@ import base from "./index.js"
 | 
			
		||||
export const admin = {
 | 
			
		||||
    ...base,
 | 
			
		||||
    AManyToManyEdit,
 | 
			
		||||
    AFileUpload, ASelectFile, AEditor,
 | 
			
		||||
    AFileUpload, ASelectFile,
 | 
			
		||||
    AFormSet, ATrackListEditor, ASoundListEditor,
 | 
			
		||||
    AStatistics, AStreamer,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -141,7 +141,7 @@ export default class PageLoad {
 | 
			
		||||
        let submit = event.type == 'submit';
 | 
			
		||||
        let target = submit || event.target.tagName == 'A'
 | 
			
		||||
                        ? event.target : event.target.closest('a');
 | 
			
		||||
        if(!target || target.hasAttribute('target') || (target.dataset && target.dataset.forceReload))
 | 
			
		||||
        if(!target || target.hasAttribute('target') || target.data.forceReload)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        let url = submit ? target.getAttribute('action') || ''
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,7 @@ input.half-field:not(:active):not(:hover) {
 | 
			
		||||
    --body-bg: #fff;
 | 
			
		||||
    --text-color: black;
 | 
			
		||||
    --text-color-light: #555;
 | 
			
		||||
    --break-color: rgb(225, 225, 225, 0.8);
 | 
			
		||||
    --break-color: rgb(225, 225, 225);
 | 
			
		||||
 | 
			
		||||
    --main-color: #EFCA08;
 | 
			
		||||
    --main-color-light: #F4da51;
 | 
			
		||||
@ -67,7 +67,7 @@ body.mobile {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media screen and (max-width: v.$screen-normal) {
 | 
			
		||||
    html { font-size: 16px !important; }
 | 
			
		||||
    html { font-size: 18px !important; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media screen and (max-width: v.$screen-wider) {
 | 
			
		||||
@ -75,7 +75,7 @@ body.mobile {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media screen and (min-width: v.$screen-wider) {
 | 
			
		||||
    html { font-size: 20px !important; }
 | 
			
		||||
    html { font-size: 24px !important; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
h1, h2, h3, h4, h5, h6, .heading, .title, .subtitle {
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,9 @@
 | 
			
		||||
@use "vars" as v;
 | 
			
		||||
 | 
			
		||||
:root {
 | 
			
		||||
    --title-1-sz: 1.4rem;
 | 
			
		||||
    --title-2-sz: 1.3rem;
 | 
			
		||||
    --title-3-sz: 1.1rem;
 | 
			
		||||
    --title-4-sz: 1.0rem;
 | 
			
		||||
    --title-1-sz: 1.6rem;
 | 
			
		||||
    --title-2-sz: 1.4rem;
 | 
			
		||||
    --title-3-sz: 1.2rem;
 | 
			
		||||
    --subtitle-1-sz: 1.6rem;
 | 
			
		||||
    --subtitle-2-sz: 1.4rem;
 | 
			
		||||
    --subtitle-3-sz: 1.2rem;
 | 
			
		||||
@ -16,17 +15,22 @@
 | 
			
		||||
    --heading-hg-bg: var(--secondary-color);
 | 
			
		||||
    --heading-link-hv-fg: var(--link-fg);
 | 
			
		||||
 | 
			
		||||
    --cover-w: 10rem;
 | 
			
		||||
    --cover-h: 10rem;
 | 
			
		||||
    --cover-w: 14rem;
 | 
			
		||||
    --cover-h: 14rem;
 | 
			
		||||
    --cover-small-w: 10rem;
 | 
			
		||||
    --cover-small-h: 10rem;
 | 
			
		||||
    --cover-tiny-w: 10rem;
 | 
			
		||||
    --cover-tiny-h: 10rem;
 | 
			
		||||
 | 
			
		||||
    --card-w: var(--cover-w);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    --preview-bg: var(--body-bg);
 | 
			
		||||
    --preview-title-sz: var(--title-4-sz);
 | 
			
		||||
    --preview-subtitle-sz: var(--title-4-sz);
 | 
			
		||||
    --preview-title-sz: var(--title-3-sz);
 | 
			
		||||
    --preview-subtitle-sz: var(--title-3-sz);
 | 
			
		||||
    --preview-cover-size: 14rem;
 | 
			
		||||
    --preview-cover-small-size: 10rem;
 | 
			
		||||
    --preview-cover-tiny-size: 4rem;
 | 
			
		||||
    --preview-wide-content-sz: #{v.$text-size-2};
 | 
			
		||||
    --preview-heading-bg-color: var(--main-color);
 | 
			
		||||
    --header-height: var(--cover-h);
 | 
			
		||||
@ -91,32 +95,7 @@
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media screen and (max-width: v.$screen-wide) {
 | 
			
		||||
    :root {
 | 
			
		||||
        --cover-w: 8rem;
 | 
			
		||||
        --cover-h: 8rem;
 | 
			
		||||
        --cover-small-w: 4rem;
 | 
			
		||||
        --cover-small-h: 4rem;
 | 
			
		||||
        --cover-tiny-w: 2rem;
 | 
			
		||||
        --cover-tiny-h: 2rem;
 | 
			
		||||
 | 
			
		||||
        --section-content-sz: 1rem;
 | 
			
		||||
 | 
			
		||||
        // --preview-title-sz: #{v.$text-size};
 | 
			
		||||
        // --preview-subtitle-sz: #{v.$text-size-smaller};
 | 
			
		||||
        // --preview-wide-content-sz: #{v.$text-size};
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ---- headings
 | 
			
		||||
 | 
			
		||||
.no-reset h1 { font-size: var(--title-1-sz); }
 | 
			
		||||
.no-reset h2 { font-size: var(--title-2-sz); }
 | 
			
		||||
.no-reset h3 { font-size: var(--title-3-sz); }
 | 
			
		||||
.no-reset h3 { font-size: var(--title-3-sz); }
 | 
			
		||||
.no-reset h4 { font-size: var(--title-4-sz); }
 | 
			
		||||
.no-reset h5 { font-size: var(--title-5-sz); }
 | 
			
		||||
 | 
			
		||||
.title, .header.preview .title {
 | 
			
		||||
    &.is-1 { font-size: var(--title-1-sz); }
 | 
			
		||||
    &.is-2 { font-size: var(--title-2-sz); }
 | 
			
		||||
@ -145,7 +124,7 @@
 | 
			
		||||
    &:not(:empty) {
 | 
			
		||||
        // border-bottom: 1px var(--heading-bg) solid;
 | 
			
		||||
        // color: var(--heading-fg);
 | 
			
		||||
        //padding: v.$mp-2;
 | 
			
		||||
        padding: v.$mp-2;
 | 
			
		||||
        margin-top: 0em !important;
 | 
			
		||||
        vertical-align: top;
 | 
			
		||||
 | 
			
		||||
@ -236,10 +215,6 @@
 | 
			
		||||
            &:last-child { border-right: 0px; }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .button-group + .button-group {
 | 
			
		||||
        border-left: 1px solid var(--text-color-light);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -318,21 +293,21 @@
 | 
			
		||||
 | 
			
		||||
    &.small, .preview.small & {
 | 
			
		||||
        min-width: unset;
 | 
			
		||||
        height: var(--cover-small-h);
 | 
			
		||||
        width: var(--cover-small-w) !important;
 | 
			
		||||
        min-width: var(--cover-small-w);
 | 
			
		||||
        height: var(--preview-cover-small-size);
 | 
			
		||||
        width: var(--preview-cover-small-size) !important;
 | 
			
		||||
        min-width: var(--preview-cover-small-size);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.tiny, .preview.tiny & {
 | 
			
		||||
        min-width: unset;
 | 
			
		||||
        height: var(--cover-tiny-h);
 | 
			
		||||
        width: var(--cover-tiny-w) !important;
 | 
			
		||||
        min-width: var(--cover-tiny-w);
 | 
			
		||||
        height: var(--preview-cover-tiny-size);
 | 
			
		||||
        width: var(--preview-cover-tiny-size) !important;
 | 
			
		||||
        min-width: var(--preview-cover-tiny-size);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.preview-header {
 | 
			
		||||
    // width: 100%;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
 | 
			
		||||
    /*&:not(.no-cover) {
 | 
			
		||||
        min-height: var(--header-height);
 | 
			
		||||
@ -390,7 +365,7 @@
 | 
			
		||||
        margin-bottom: unset;
 | 
			
		||||
 | 
			
		||||
        .list-item:not(.no-cover) & {
 | 
			
		||||
            min-height: var(--cover-small-h);
 | 
			
		||||
            min-height: var(--preview-cover-small-size);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -138,9 +138,6 @@
 | 
			
		||||
.bg-secondary-light { background-color: var(--secondary-color-light); }
 | 
			
		||||
.bg-transparent { background-color: transparent; }
 | 
			
		||||
 | 
			
		||||
.border { border: 1px solid var(--text-color); }
 | 
			
		||||
.border-main { border: 1px solid var(--main-color); }
 | 
			
		||||
.border-secondary { border: 1px solid var(--secondary-color); }
 | 
			
		||||
.border-bottom-main { border-bottom: 1px solid var(--main-color); }
 | 
			
		||||
.border-bottom-secondary { border-bottom: 1px solid var(--secondary-color); }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -22,7 +22,7 @@
 | 
			
		||||
 | 
			
		||||
        &:not(:last-child) {
 | 
			
		||||
            padding-bottom: calc(v.$mp-4 / 2);
 | 
			
		||||
            // border-bottom: 2px var(--break-color) solid;
 | 
			
		||||
            border-bottom: 2px var(--break-color) solid;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        > .title, h3.title {
 | 
			
		||||
@ -60,8 +60,7 @@
 | 
			
		||||
    margin: v.$mp-3;
 | 
			
		||||
    margin-left: 0rem;
 | 
			
		||||
    padding: v.$mp-2;
 | 
			
		||||
    text-color: var(--main-color);
 | 
			
		||||
    background-color: var(--main-color-light);
 | 
			
		||||
    border-bottom: 1px var(--main-color) solid;
 | 
			
		||||
 | 
			
		||||
    .heading {
 | 
			
		||||
        padding: 0em;
 | 
			
		||||
 | 
			
		||||
@ -77,6 +77,99 @@ except Exception:
 | 
			
		||||
# -- django-taggit
 | 
			
		||||
TAGGIT_CASE_INSENSITIVE = True
 | 
			
		||||
 | 
			
		||||
# -- django-CKEditor
 | 
			
		||||
CKEDITOR_CONFIGS = {
 | 
			
		||||
    "default": {
 | 
			
		||||
        "format_tags": "h1;h2;h3;p;pre",
 | 
			
		||||
        # 'skin': 'office2013',
 | 
			
		||||
        "toolbar_Custom": [
 | 
			
		||||
            {
 | 
			
		||||
                "name": "editing",
 | 
			
		||||
                "items": [
 | 
			
		||||
                    "Undo",
 | 
			
		||||
                    "Redo",
 | 
			
		||||
                    "-",
 | 
			
		||||
                    "Find",
 | 
			
		||||
                    "Replace",
 | 
			
		||||
                    "-",
 | 
			
		||||
                    "Source",
 | 
			
		||||
                ],
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                "name": "basicstyles",
 | 
			
		||||
                "items": [
 | 
			
		||||
                    "Bold",
 | 
			
		||||
                    "Italic",
 | 
			
		||||
                    "Underline",
 | 
			
		||||
                    "Strike",
 | 
			
		||||
                    "Subscript",
 | 
			
		||||
                    "Superscript",
 | 
			
		||||
                    "-",
 | 
			
		||||
                    "RemoveFormat",
 | 
			
		||||
                ],
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                "name": "paragraph",
 | 
			
		||||
                "items": [
 | 
			
		||||
                    "NumberedList",
 | 
			
		||||
                    "BulletedList",
 | 
			
		||||
                    "-",
 | 
			
		||||
                    "Outdent",
 | 
			
		||||
                    "Indent",
 | 
			
		||||
                    "-",
 | 
			
		||||
                    "Blockquote",
 | 
			
		||||
                    "CreateDiv",
 | 
			
		||||
                    "-",
 | 
			
		||||
                    "JustifyLeft",
 | 
			
		||||
                    "JustifyCenter",
 | 
			
		||||
                    "JustifyRight",
 | 
			
		||||
                    "JustifyBlock",
 | 
			
		||||
                    "-",
 | 
			
		||||
                ],
 | 
			
		||||
            },
 | 
			
		||||
            "/",
 | 
			
		||||
            {"name": "links", "items": ["Link", "Unlink", "Anchor"]},
 | 
			
		||||
            {
 | 
			
		||||
                "name": "insert",
 | 
			
		||||
                "items": [
 | 
			
		||||
                    "Image",
 | 
			
		||||
                    "Table",
 | 
			
		||||
                    "HorizontalRule",
 | 
			
		||||
                    "SpecialChar",
 | 
			
		||||
                    "PageBreak",
 | 
			
		||||
                    "Iframe",
 | 
			
		||||
                ],
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                "name": "styles",
 | 
			
		||||
                "items": ["Styles", "Format", "Font", "FontSize"],
 | 
			
		||||
            },
 | 
			
		||||
            {"name": "colors", "items": ["TextColor", "BGColor"]},
 | 
			
		||||
            "/",  # put this to force next toolbar on new line
 | 
			
		||||
        ],
 | 
			
		||||
        "toolbar": "Custom",
 | 
			
		||||
        "extraPlugins": ",".join(
 | 
			
		||||
            [
 | 
			
		||||
                "uploadimage",
 | 
			
		||||
                "div",
 | 
			
		||||
                "autolink",
 | 
			
		||||
                "autoembed",
 | 
			
		||||
                "embedsemantic",
 | 
			
		||||
                "embed",
 | 
			
		||||
                "iframe",
 | 
			
		||||
                "iframedialog",
 | 
			
		||||
                "autogrow",
 | 
			
		||||
                "widget",
 | 
			
		||||
                "lineutils",
 | 
			
		||||
                "dialog",
 | 
			
		||||
                "dialogui",
 | 
			
		||||
                "elementspath",
 | 
			
		||||
            ]
 | 
			
		||||
        ),
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
CKEDITOR_UPLOAD_PATH = "uploads/"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# -- easy_thumbnails
 | 
			
		||||
THUMBNAIL_PROCESSORS = (
 | 
			
		||||
@ -97,6 +190,8 @@ INSTALLED_APPS = (
 | 
			
		||||
    "rest_framework",
 | 
			
		||||
    "django_filters",
 | 
			
		||||
    "content_editor",
 | 
			
		||||
    "ckeditor",
 | 
			
		||||
    "ckeditor_uploader",
 | 
			
		||||
    "easy_thumbnails",
 | 
			
		||||
    "filer",
 | 
			
		||||
    "taggit",
 | 
			
		||||
 | 
			
		||||
@ -27,6 +27,7 @@ urlpatterns = [
 | 
			
		||||
    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")),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
{% extends "aircox/base.html" %}
 | 
			
		||||
{% load static %}
 | 
			
		||||
 | 
			
		||||
{% block head %}
 | 
			
		||||
{% block assets %}
 | 
			
		||||
{{ block.super }}
 | 
			
		||||
<style>
 | 
			
		||||
:root {
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ django-filer~=3.1
 | 
			
		||||
django-honeypot~=1.0
 | 
			
		||||
django-taggit~=3.0
 | 
			
		||||
django-admin-sortable2~=2.1
 | 
			
		||||
django-ckeditor~=6.4
 | 
			
		||||
bleach~=5.0
 | 
			
		||||
easy-thumbnails~=2.8
 | 
			
		||||
tzlocal~=4.2
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user