Compare commits

..

149 Commits

Author SHA1 Message Date
6df6523062 merge develop-1.0 2024-04-28 21:06:47 +02:00
8208554c4b i18n 2024-04-28 20:58:45 +02:00
87692c860b streamer: integrate, fix, ui change 2024-04-28 18:59:33 +02:00
a64c850efa cleanup 2024-04-28 00:30:28 +02:00
881d518acb cleanup admin 2024-04-28 00:27:26 +02:00
919cb06da8 fix rendering 2024-04-27 21:36:24 +02:00
8f1ec9cbc1 M2M list editor; users list & group mgt; add missing files 2024-04-27 21:05:36 +02:00
ae176dc623 color info 2024-04-23 00:07:09 +02:00
0512533244 assets 2024-04-22 23:57:32 +02:00
a2a399e531 manage program editors 2024-04-22 23:54:44 +02:00
b28105c659 vue 3.4 2024-04-21 23:50:00 +02:00
07d72d799d rm file 2024-04-19 15:06:23 +02:00
1d321a0de6 imrpove statistics rendering + filters 2024-04-12 16:49:05 +02:00
8a6f72ca83 imrpove statistics rendering + filters 2024-04-12 16:36:05 +02:00
0ee72f30c5 integrate statistics 2024-04-12 15:48:55 +02:00
7841fed17d preview.tiny; dashboard list order 2024-04-11 00:33:05 +02:00
1bd4e03f02 add missing files; improve dashboard; rewrite urls 2024-04-11 00:28:43 +02:00
a24318bc84 #137: Sound et EpisodeSound, dashboard UI improvements (into #121) (#138)
#137

Deployment: **Upgrade to Liquidsoap 2.4**: code has been adapted to work with liquidsoap 2.4

Co-authored-by: bkfox <thomas bkfox net>
Reviewed-on: #138
2024-04-05 18:45:15 +02:00
bda4efe336 rows fix 2024-03-26 01:19:20 +01:00
c3c748eebb delete sound in list 2024-03-26 00:36:51 +01:00
3fb9e0d62a make EpisodeUpdateView work 2024-03-25 23:50:08 +01:00
8d4b4c5896 make EpisodeUpdateView work 2024-03-25 23:48:25 +01:00
1f716891ac work on sound list 2024-03-25 18:07:38 +01:00
70a55607a5 work on sound list 2024-03-25 18:05:55 +01:00
f41cc3ce0c work on playlist & tracklist 2024-03-23 17:22:52 +01:00
21f856e731 page_form: some fields horizontal 2024-03-20 01:55:36 +01:00
d293eb4a00 rename playlist-editor to tracklist-editor; refactor player 2024-03-20 01:42:01 +01:00
3ad886764c upload selector improvements 2024-03-17 21:14:26 +01:00
024db5f307 upload selector improvements 2024-03-17 21:00:07 +01:00
de858f45e8 work on episode form 2024-03-16 22:37:56 +01:00
eaf453086d cover into modal; add Dashboard js app 2024-03-16 08:06:32 +01:00
3c56dc8b53 fix assets 2024-03-16 07:24:12 +01:00
44b9a608ee work on page form; add image selector 2024-03-16 06:02:23 +01:00
eb5bdcf167 work on page form; add image selector 2024-03-16 06:00:15 +01:00
c74ec6fb16 Merge branch 'dev-1.0-118-design' into dev-1.0-121 2024-03-15 20:54:56 +01:00
c79f040fa1 fix 2024-03-15 20:54:52 +01:00
7cdf44b901 templates: always display profile link 2024-03-14 09:25:27 +01:00
dff7b1cf8c update translations 2024-03-14 09:22:03 +01:00
f55d747034 templates/episode_form: add an horizontal rule 2024-03-14 09:04:30 +01:00
37ecf9875b templates/edit-link: display detail view link; use small font size 2024-03-14 08:59:39 +01:00
f8401c76e3 templates/edit-link: use fontawesome icons 2024-03-14 08:54:51 +01:00
306eb20257 docs: update user manual with simplified program management for animators 2024-02-28 17:19:21 +01:00
5ae85083a5 db: create program editors groups 2024-02-28 17:19:21 +01:00
1ac83f1066 db: add missing migration on schedule timezone 2024-02-28 17:19:21 +01:00
7e0e6e9652 episode-form: add tracks inline formset 2024-02-28 17:19:21 +01:00
ab1b152a46 templatetags: display edit-links for admins 2024-02-28 17:19:21 +01:00
8821cd86c6 templates: update after merging branch 118-design 2024-02-28 17:19:21 +01:00
4ea93c9eff templates: add in-context edition links 2024-02-28 17:19:21 +01:00
40ca2064d9 db: migrations merge 2024-02-28 17:19:21 +01:00
e840fbabac templates: update container block names 2024-02-28 17:19:21 +01:00
ff2a8ff6d4 signals: disable schedule_pre_save when using loaddata 2024-02-28 17:19:21 +01:00
d33256edb8 templates: set document type to html, prevent quicks mode 2024-02-28 17:19:21 +01:00
10b9e9280f context_processors: prevent a null station error when no default station is defined 2024-02-28 17:19:21 +01:00
ee7f301f44 views/program: allow changing program cover 2024-02-28 17:19:21 +01:00
64984d69d5 misc: add a profile view for authenticated users 2024-02-28 17:19:21 +01:00
4f856c0705 misc: use the django authentication system 2024-02-28 17:19:21 +01:00
8b4da52760 misc: move station and audio_streams to context_processors (in order to have them available in accounts views) 2024-02-28 17:19:21 +01:00
aa171375e5 misc: edit programs in site 2024-02-28 17:19:18 +01:00
1674266890 templatetags: parametrize has_perm() in order to enable aircox namespace permissions 2024-02-28 15:17:38 +01:00
dd71f984ed models/program: link to editor groups 2024-02-28 15:17:31 +01:00
0ba0f8ae72 mobile device support 2024-02-12 23:28:59 +01:00
afc2e41bdb Merge branch 'dev-1.0-118-design' of git.radiocampus.be:rc/aircox into dev-1.0-118-design 2024-02-12 14:41:19 +01:00
bba4935791 grid and mobile 2024-02-12 14:41:09 +01:00
dab4146735 box shadow 2024-02-12 14:40:43 +01:00
1aababe2ae docs: update user manual with simplified program management for animators 2024-02-06 09:57:21 +01:00
0dd961e0bb db: create program editors groups 2024-02-06 09:57:20 +01:00
f9da318a38 db: add missing migration on schedule timezone 2024-02-06 09:57:20 +01:00
26fa426416 views: avoid failing on missing parent cover 2024-02-06 09:40:45 +01:00
71f4d2473e episode-form: add tracks inline formset 2024-02-06 09:40:45 +01:00
2e9ebaded2 templatetags: display edit-links for admins 2024-02-06 09:40:45 +01:00
c6a4196319 templates: update after merging branch 118-design 2024-02-06 09:40:37 +01:00
be224d0efb templatetags: return on none type object 2024-02-05 10:29:58 +01:00
89f80ad103 templates: add in-context edition links 2024-02-05 10:29:58 +01:00
6d556fcd5d db: migrations merge 2024-02-05 10:29:55 +01:00
4201d50f4b templates: update container block names 2024-02-05 10:24:48 +01:00
6c942f36fa templatetags: avoid failing on nav_items when no station is defined 2024-02-05 10:24:48 +01:00
d51b9ee58b signals: disable schedule_pre_save when using loaddata 2024-02-05 10:24:48 +01:00
1a27ae2a76 misc: add in-site episode management for animators 2024-02-05 10:24:46 +01:00
e5862ee59b templates: set document type to html, prevent quicks mode 2024-02-05 10:22:16 +01:00
8f88b15536 ProgramUpdateView: use ckeditor RichTextField 2024-02-05 10:22:16 +01:00
10dfe3811b context_processors: prevent a null station error when no default station is defined 2024-02-05 10:22:16 +01:00
f71c201020 views/program: allow changing program cover 2024-02-05 10:22:16 +01:00
0812f3a0a1 misc: add a profile view for authenticated users 2024-02-05 10:22:14 +01:00
269b29b2c1 aircox/conf: user cannot edit all programs/episode 2024-02-05 10:19:05 +01:00
ad2ed17c34 misc: use the django authentication system 2024-02-05 10:19:05 +01:00
9db69580e0 misc: move station and audio_streams to context_processors (in order to have them available in accounts views) 2024-02-05 10:19:05 +01:00
4ead6b154b misc: edit programs in site 2024-02-05 10:19:05 +01:00
811cc97e07 templatetags: parametrize has_perm() in order to enable aircox namespace permissions 2024-02-05 10:19:05 +01:00
b794e24d0c models/program: link to editor groups 2024-02-05 10:19:05 +01:00
df41885cca various fixes 2024-02-02 19:36:02 +01:00
2a75608701 admin rendering 2024-02-01 20:01:57 +01:00
e1cf455384 page glitch 2024-02-01 19:45:14 +01:00
93e286fa62 music stream 2024-02-01 19:31:30 +01:00
e3966ca5cb logs 2024-02-01 18:15:24 +01:00
c335ed9fb9 logs 2024-02-01 17:58:42 +01:00
ad90255570 design 2024-02-01 17:29:35 +01:00
cab6cacd0b design / mockups 2024-01-31 20:24:38 +01:00
1475a80316 fixes 2024-01-30 20:19:44 +01:00
b9148933f4 rendering 2024-01-27 19:24:24 +01:00
5bb52a9d67 rendering 2024-01-27 18:56:05 +01:00
8cf57c07b2 rendering 2024-01-26 22:43:16 +01:00
20aa3aba9d rendering 2024-01-26 22:28:08 +01:00
d53cb3e935 missing file 2024-01-26 21:56:56 +01:00
25ceacdff9 work on main design & layout 2024-01-26 21:55:43 +01:00
0adcacf375 merge upstream 2024-01-24 16:20:18 +01:00
c31d776504 breadcrumbs; urls 2024-01-24 16:13:35 +01:00
69b77a675b breadcrumbs 2024-01-24 16:13:02 +01:00
ac9b3c8ede rendering styles 2024-01-16 15:37:04 +01:00
825ed03dbd rendering styles; view order; i18n 2024-01-16 14:36:09 +01:00
561914ee78 bkp 2024-01-09 20:05:39 +01:00
ccea2a5ea6 player link; page rendering 2024-01-05 19:17:10 +01:00
c52e87acd2 home page fixes; various issues fix 2024-01-05 16:23:23 +01:00
294c848415 player button & playlist header fix; timetable order 2024-01-03 19:51:39 +01:00
1f6381bf07 migration files 2023-12-13 17:27:42 +01:00
73d8ff32d5 fix bug & remove dynamic 2023-12-12 21:24:21 +01:00
46a9008cda navigation & breadcrumbs 2023-12-12 20:07:58 +01:00
eaa1e2412a page headers, various fixes, responsive 2023-12-11 23:29:49 +01:00
a3c21c64ed fix integration into admin interface 2023-12-10 15:47:04 +01:00
0e444f0502 fix integration into admin interface 2023-12-10 15:21:30 +01:00
4778803ee0 page headers 2023-12-01 20:50:28 +01:00
9c3eaf05c7 page headers 2023-12-01 20:49:34 +01:00
f05e47af1c page headers 2023-12-01 20:43:12 +01:00
1de9548111 player shadow 2023-11-29 15:45:22 +01:00
8202a9324c responsive menus 2023-11-29 15:41:15 +01:00
f5ce00795e page loader 2023-11-29 02:05:14 +01:00
4e04cfae7e add pdocasts 2023-11-28 02:36:24 +01:00
d2ed8df2ac add pdocasts 2023-11-28 02:22:58 +01:00
712ab223ba add pdocasts 2023-11-28 02:16:40 +01:00
ed9affbef6 carousel, display logs 2023-11-28 01:23:56 +01:00
cb5a6a3ee8 carousel, display logs 2023-11-28 01:04:39 +01:00
bc697bd4bd clean-up css; related publications; pagination 2023-11-26 21:35:37 +01:00
d075fecbce attach static page to page-list 2023-11-24 22:11:55 +01:00
0c07586787 player: progress bar position 2023-11-24 21:56:58 +01:00
9661e98a70 player: progress bar position 2023-11-24 21:39:20 +01:00
69d77e1d0c player: progress bar position 2023-11-24 21:27:59 +01:00
62ada47352 podcasts & player 2023-11-24 20:46:56 +01:00
474016f776 merge develop-1.0 2023-11-22 21:17:37 +01:00
6a21a9d094 design 2023-11-22 21:09:59 +01:00
b4c12def13 work on templates 2023-11-22 17:33:51 +01:00
36ae12af3d fix: static 2023-11-02 22:10:41 +01:00
0a86d4e0a3 add statics 2023-11-02 22:09:49 +01:00
a53aebb5b8 rm static 2023-11-02 22:07:27 +01:00
1af0348c89 add vue files 2023-11-02 22:03:55 +01:00
8ab8ef5b1c add missing files 2023-11-02 21:59:30 +01:00
bf9da835b2 add missing files 2023-11-02 21:58:13 +01:00
7b28149d7e add radiocampus app 2023-11-02 21:56:22 +01:00
87a2ee5a45 feat: work on date menu 2023-11-02 21:54:15 +01:00
ab231e9a89 work on design 2023-10-27 21:09:58 +02:00
1661601caf work on design: items, components 2023-10-24 18:29:34 +02:00
53 changed files with 348 additions and 494 deletions

View File

@ -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")

View File

@ -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

View File

@ -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",

View File

@ -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"),
),
]

View File

@ -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"
),
),
]

View File

@ -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

View File

@ -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)

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 %}
&nbsp;
{% 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>

View 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

View 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")

View 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)

View File

@ -44,7 +44,6 @@ class TestBaseView:
"station": station,
"page": None, # get_page() returns None
"model": base_view.model,
"nav_menu": [],
}

View File

@ -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

View File

@ -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):

View File

@ -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)

View File

@ -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"))

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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>

View File

@ -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,
)

View File

@ -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",

View File

@ -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>

View File

@ -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) {

View File

@ -274,6 +274,7 @@ export default {
if(event.type == 'ended' && (!this.playlist || this.playlist.selectNext() == -1))
this.play();
},
},
mounted() {

View File

@ -29,7 +29,7 @@ import ASoundItem from './ASoundItem';
export default {
extends: AList,
emits: [...AList.emits],
emits: [...AList.emits, 'remove'],
components: { ASoundItem },
props: {

View File

@ -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,
}

View File

@ -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') || ''

View File

@ -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 {

View File

@ -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);
}
}

View File

@ -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); }

View File

@ -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;

View File

@ -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",

View File

@ -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")),
]

View File

@ -1,7 +1,7 @@
{% extends "aircox/base.html" %}
{% load static %}
{% block head %}
{% block assets %}
{{ block.super }}
<style>
:root {

View File

@ -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